mixdog 0.7.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 (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. package/tools.json +1653 -0
@@ -0,0 +1,869 @@
1
+ import { statSync } from 'fs';
2
+ import { isAbsolute, resolve } from 'path';
3
+ import { trueCasePath } from './path-utils.mjs';
4
+ import {
5
+ canonicalizeGlobSlashes,
6
+ coerceShapeFlex,
7
+ extractGlobBaseDirectory,
8
+ hasGlobMagic,
9
+ normalizeGlobArgs,
10
+ normalizeGrepArgs,
11
+ normalizeInputPath,
12
+ normalizeOutputPath,
13
+ normalizeSearchPattern,
14
+ resolveAgainstCwd,
15
+ } from './path-utils.mjs';
16
+ import {
17
+ buildGlobCacheKey,
18
+ buildGrepCacheKey,
19
+ buildGrepRgArgs,
20
+ DEFAULT_IGNORE_GLOBS,
21
+ } from './search-builders.mjs';
22
+ import { runRg, runRgWindowedLines } from './rg-runner.mjs';
23
+ import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
24
+ import {
25
+ groupGrepContentByFile,
26
+ normalizeGrepLine,
27
+ splitGrepCountPrefix,
28
+ splitGrepLinePrefix,
29
+ } from './grep-formatting.mjs';
30
+ import {
31
+ cacheGet,
32
+ cacheSet,
33
+ statPathsForMtime,
34
+ } from './cache-layers.mjs';
35
+ import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
36
+ import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
37
+
38
+ // Deterministic ENOENT recovery: when a grep path does not exist, surface
39
+ // indexed files that share the missing path's basename, turning a guessed or
40
+ // misplaced path (e.g. session/result-compression.mjs vs the real
41
+ // tools/result-compression.mjs) into the actual file in one step. Exact
42
+ // basename only — no stem/token fuzzing — so the hint is high-signal and
43
+ // noise-free. Invariant: every ENOENT runs the same basename lookup; there is
44
+ // no "guessed a lot" branch. Returns '' (appends nothing) when no same-named
45
+ // indexed file exists or the glob child is unavailable.
46
+ async function _suggestIndexedPaths(missingPath, executeChildBuiltinTool, workDir) {
47
+ if (typeof executeChildBuiltinTool !== 'function') return '';
48
+ const base = String(missingPath).replace(/\\/g, '/').split('/').pop();
49
+ // Skip when there is no usable basename or it carries glob magic (the
50
+ // pattern, not a literal filename, would not map to a real file).
51
+ if (!base || /[*?[\]{}]/.test(base)) return '';
52
+ try {
53
+ const out = await executeChildBuiltinTool('glob', { pattern: `**/${base}`, head_limit: 6 }, workDir);
54
+ if (typeof out !== 'string') return '';
55
+ // Drop ONLY the exact diagnostic forms glob emits: the empty-result line
56
+ // ("(no files found ...", "(no entries after offset ...") and the
57
+ // suffix/warning lines ("... [N more entries]", "... [warning] ...").
58
+ // A broad startsWith('(' / '...' / 'Error') would wrongly drop real
59
+ // paths like ErrorBoundary.mjs or a "(draft)/x.mjs" leading segment.
60
+ const hits = out.split('\n')
61
+ .filter((s) => s && !/^\(no (?:files found|entries\b)/.test(s) && !/^\.\.\. \[/.test(s))
62
+ .map((s) => s.trim())
63
+ .filter(Boolean)
64
+ .slice(0, 5);
65
+ return hits.length ? `\n[path not found here; same-named indexed file(s): ${hits.join(', ')}]` : '';
66
+ } catch {
67
+ return '';
68
+ }
69
+ }
70
+
71
+ function relativePathPrefix(pathPrefix, workDir) {
72
+ if (!workDir) return pathPrefix;
73
+ const cwdFwd = workDir.replace(/\\/g, '/').replace(/\/+$/, '');
74
+ const absFwd = String(pathPrefix || '').replace(/\\/g, '/');
75
+ const haystack = process.platform === 'win32' ? absFwd.toLocaleLowerCase() : absFwd;
76
+ const needle = process.platform === 'win32' ? cwdFwd.toLocaleLowerCase() : cwdFwd;
77
+ if (haystack.startsWith(needle + '/') || haystack === needle) {
78
+ return absFwd.slice(cwdFwd.length + 1) || '.';
79
+ }
80
+ return pathPrefix;
81
+ }
82
+
83
+ function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content', filenameOmitted = false) {
84
+ const normalized = normalizeGrepLine(line, pathOnly, outputMode, filenameOmitted);
85
+ if (!workDir) return normalized;
86
+ if (pathOnly) return relativePathPrefix(normalized, workDir);
87
+ if (filenameOmitted) return normalized;
88
+ const split = splitGrepLinePrefix(normalized);
89
+ if (split) {
90
+ return relativePathPrefix(normalized.slice(0, split.pathEnd), workDir) + normalized.slice(split.pathEnd);
91
+ }
92
+ if (outputMode === 'count') {
93
+ const countSplit = splitGrepCountPrefix(normalized);
94
+ if (countSplit) {
95
+ return relativePathPrefix(normalized.slice(0, countSplit.pathEnd), workDir) + normalized.slice(countSplit.pathEnd);
96
+ }
97
+ }
98
+ return normalized;
99
+ }
100
+
101
+ function relativeSearchResultPath(path, workDir) {
102
+ const normalizedWorkDir = normalizeOutputPath(workDir);
103
+ const normalizedAbs = normalizeOutputPath(path);
104
+ if (normalizedAbs.startsWith(normalizedWorkDir + '/') || normalizedAbs.startsWith(normalizedWorkDir + '\\')) {
105
+ return normalizedAbs.slice(normalizedWorkDir.length + 1);
106
+ }
107
+ return normalizedAbs;
108
+ }
109
+
110
+ function uniqueStrings(values) {
111
+ return Array.from(new Set(values.filter((value) => typeof value === 'string' && value)));
112
+ }
113
+
114
+ function coerceNonNegInt(value) {
115
+ if (value === undefined || value === null || value === '') return null;
116
+ const n = Number(value);
117
+ if (!Number.isFinite(n) || n < 0) return NaN;
118
+ return Math.floor(n);
119
+ }
120
+
121
+ function globMtimeTiePath(entry) {
122
+ const p = String(entry?.path ?? entry?.full ?? '');
123
+ return process.platform === 'win32' ? p.toLocaleLowerCase() : p;
124
+ }
125
+
126
+ // CC parity (GrepTool.ts): a single glob string may pack multiple filters
127
+ // separated by whitespace or commas, e.g. "*.ts,*.tsx" or "*.ts *.tsx". Split
128
+ // each into its own --glob. Brace patterns ("*.{ts,tsx}") are left intact so
129
+ // their internal commas are not torn apart.
130
+ function splitGlobString(value) {
131
+ const out = [];
132
+ const str = String(value);
133
+ let depth = 0;
134
+ let token = '';
135
+ const flush = () => {
136
+ const trimmed = token.trim();
137
+ if (trimmed) out.push(trimmed);
138
+ token = '';
139
+ };
140
+ for (const ch of str) {
141
+ if (ch === '{') {
142
+ depth++;
143
+ token += ch;
144
+ } else if (ch === '}') {
145
+ if (depth > 0) depth--;
146
+ token += ch;
147
+ } else if (depth === 0 && (ch === ',' || /\s/.test(ch))) {
148
+ flush();
149
+ } else {
150
+ token += ch;
151
+ }
152
+ }
153
+ flush();
154
+ return out;
155
+ }
156
+
157
+ function resolveSearchScope(root, workDir) {
158
+ return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
159
+ }
160
+
161
+ function isUncOrSmbPath(path) {
162
+ if (typeof path !== 'string' || !path) return false;
163
+ return path.startsWith('\\\\') || path.startsWith('//');
164
+ }
165
+
166
+ function uncRefusalMessage(toolName, original, resolved) {
167
+ const shown = normalizeOutputPath(resolved || original || '');
168
+ return `Error: ${toolName} refuses UNC/SMB path ${JSON.stringify(shown)}; remote share access is blocked to prevent NTLM credential leaks`;
169
+ }
170
+
171
+ function basePathDiagnostic(basePaths, workDir) {
172
+ return basePaths.map((basePath) => {
173
+ const resolved = resolveSearchScope(basePath, workDir);
174
+ try {
175
+ const st = statSync(resolved);
176
+ return `${normalizeOutputPath(basePath)}: ${st.isDirectory() ? 'path exists (dir)' : 'path exists (file)'}`;
177
+ } catch (err) {
178
+ return `${normalizeOutputPath(basePath)}: path does not exist (${err?.code || 'ENOENT'})`;
179
+ }
180
+ }).join('; ');
181
+ }
182
+
183
+ function grepMissingPatternMessage() {
184
+ return 'Error: grep requires pattern.';
185
+ }
186
+
187
+ function globMissingPatternMessage() {
188
+ return 'Error: glob requires pattern.';
189
+ }
190
+
191
+ function parseGrepCountLine(line) {
192
+ const text = String(line || '');
193
+ const searchFrom = /^[A-Za-z]:/.test(text) ? 2 : 0;
194
+ const idx = text.lastIndexOf(':');
195
+ if (idx <= searchFrom) return null;
196
+ const count = Number(text.slice(idx + 1));
197
+ if (!Number.isFinite(count) || count <= 0) return null;
198
+ const path = text.slice(0, idx);
199
+ if (!path) return null;
200
+ return { path, count };
201
+ }
202
+
203
+ // Per-pattern file-count probe for array grby: runs ONE rg --files-with-matches
204
+ // per pattern (reusing the same scope/glob/type/flags as the merged search) so
205
+ // the summary line can surface patterns that matched zero files — otherwise the
206
+ // merged result hides which member of the array contributed nothing.
207
+ async function _perPatternFileCounts({ patterns, searchPath, globPatterns, caseInsensitive, multilineMode, fileType, workDir }) {
208
+ const results = await Promise.all(patterns.map(async (pattern) => {
209
+ try {
210
+ const rgArgs = buildGrepRgArgs({
211
+ patterns: [pattern],
212
+ searchPath,
213
+ globPatterns,
214
+ outputMode: 'files_with_matches',
215
+ caseInsensitive,
216
+ showLineNumbers: false,
217
+ beforeN: null,
218
+ afterN: null,
219
+ contextN: null,
220
+ multilineMode,
221
+ fileType,
222
+ onlyMatching: false,
223
+ });
224
+ const stdout = await runRg(rgArgs, { cwd: workDir });
225
+ // A boxed partial/truncated stdout means the count is not the true
226
+ // total — render '?' rather than a misleadingly-low or zero number.
227
+ if (stdout && typeof stdout === 'object' && (stdout.partial || stdout.truncated)) return null;
228
+ return String(stdout).split('\n').filter(Boolean).length;
229
+ } catch {
230
+ // Probe threw → count unknown, NOT zero.
231
+ return null;
232
+ }
233
+ }));
234
+ // Quote first (so newline/control chars can't inject footer lines), then
235
+ // truncate the quoted string to 40 chars — matches the no-match path.
236
+ const trunc = (p) => {
237
+ const q = JSON.stringify(p);
238
+ return q.length > 40 ? `${q.slice(0, 40)}...` : q;
239
+ };
240
+ return `\n# per-pattern: ${patterns.map((p, i) => `${trunc(p)}=${results[i] === null ? '?' : `${results[i]} files`}`).join(', ')}`;
241
+ }
242
+
243
+ function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offset, outputMode, patterns, beforeN, afterN, contextN, searchPath, grepResolvedPath, workDir, globPatterns, fileType, filenameOmitted = false, prefix = '', broadAdvisory = true }) {
244
+ const lines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
245
+ const normalized = lines.map((line) => relativeGrepLine(line, workDir, outputMode === 'files_with_matches', outputMode, filenameOmitted));
246
+ const remaining = Math.max(0, totalWindowed - lines.length);
247
+ const shown = lines.length;
248
+ const total = totalWindowed;
249
+ const scopePath = JSON.stringify(normalizeOutputPath(searchPath));
250
+ const truncated = (remaining > 0 || !totalKnown)
251
+ ? (totalKnown
252
+ ? `\n[Showing ${shown} of ${total} results; pass offset:${offset + shown} for more]`
253
+ : `\n[Showing ${shown} (more matches exist — use output_mode:'count' for the exact total on ${scopePath}); pass offset:${offset + shown} for more]`)
254
+ : '';
255
+
256
+ let countSummary = '';
257
+ if (outputMode === 'count') {
258
+ let totalMatches = 0;
259
+ let fileCount = 0;
260
+ for (const line of normalized) {
261
+ const m = line.match(/(?:^|:)(\d+)$/);
262
+ if (m) { totalMatches += Number(m[1]); fileCount++; }
263
+ }
264
+ countSummary = `\n[total ${totalMatches} match${totalMatches === 1 ? '' : 'es'} across ${fileCount} file${fileCount === 1 ? '' : 's'}]`;
265
+ }
266
+ const hasContext = (beforeN > 0 || afterN > 0 || contextN > 0);
267
+ const groupedBody = (outputMode === 'content' && !hasContext && !filenameOmitted)
268
+ ? groupGrepContentByFile(normalized)
269
+ : normalized.join('\n');
270
+ const body = groupedBody + truncated + countSummary;
271
+ return `${prefix}${body}`;
272
+ }
273
+
274
+ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, readStateScope = null, options = {}) {
275
+ args = normalizeGrepArgs(args);
276
+ // Shape context immediately before deriving rg flags. This keeps the
277
+ // Lead-direct MCP path and direct executeGrepTool callers on the same
278
+ // policy even if they bypass or race the outer builtin arg guard.
279
+ applyGrepContextLeadPolicy(args);
280
+ args.path = normalizeInputPath(args.path);
281
+ args.pattern = coerceShapeFlex(args.pattern);
282
+ args.glob = coerceShapeFlex(args.glob);
283
+ const rawPattern = args.pattern;
284
+ const patterns = uniqueStrings((Array.isArray(rawPattern)
285
+ ? rawPattern.filter(p => typeof p === 'string' && p)
286
+ : (rawPattern ? [String(rawPattern)] : [])).map(normalizeSearchPattern));
287
+ if (patterns.length === 0) {
288
+ if (args.glob || hasGlobMagic(args.path)) {
289
+ const globArgs = {
290
+ pattern: hasGlobMagic(args.path) ? args.path : args.glob,
291
+ path: hasGlobMagic(args.path) ? undefined : (args.path || '.'),
292
+ };
293
+ if (args.head_limit !== undefined) globArgs.head_limit = args.head_limit;
294
+ if (args.offset !== undefined) globArgs.offset = args.offset;
295
+ return executeChildBuiltinTool('glob', globArgs, workDir);
296
+ }
297
+ return grepMissingPatternMessage();
298
+ }
299
+
300
+ const GREP_MULTILINE_PATTERN_CAP = 5;
301
+ const GREP_ARRAY_PATTERN_CAP = 20;
302
+ const multilineMode = args.multiline === true;
303
+ if (multilineMode && patterns.length > GREP_MULTILINE_PATTERN_CAP) {
304
+ return `Error: multiline:true with more than ${GREP_MULTILINE_PATTERN_CAP} patterns is not allowed (got ${patterns.length}); split into separate grep calls`;
305
+ }
306
+ if (patterns.length > GREP_ARRAY_PATTERN_CAP) {
307
+ return `Error: pattern array exceeds the ${GREP_ARRAY_PATTERN_CAP}-pattern cap (got ${patterns.length}); split into separate grep calls`;
308
+ }
309
+
310
+ let searchPath = args.path || '.';
311
+ const rawGlob = args.glob;
312
+ const rawGlobs = uniqueStrings((Array.isArray(rawGlob)
313
+ ? rawGlob.filter(g => typeof g === 'string' && g)
314
+ : (rawGlob ? [String(rawGlob)] : []))
315
+ .flatMap(splitGlobString)
316
+ .map(normalizeInputPath));
317
+ if (hasGlobMagic(searchPath)) {
318
+ const { baseDir, relativePattern } = extractGlobBaseDirectory(searchPath);
319
+ searchPath = baseDir || '.';
320
+ rawGlobs.unshift(relativePattern.replace(/^\//, ''));
321
+ }
322
+ const grepResolvedPath = resolveSearchScope(searchPath, workDir);
323
+ if (isUncOrSmbPath(searchPath) || isUncOrSmbPath(grepResolvedPath)) {
324
+ return uncRefusalMessage('grep', searchPath, grepResolvedPath);
325
+ }
326
+ const globPatterns = [];
327
+ const rootFwd = normalizeOutputPath(grepResolvedPath).replace(/\/+$/, '');
328
+ for (const g of rawGlobs) {
329
+ if (isAbsolute(g)) {
330
+ const { baseDir, relativePattern } = extractGlobBaseDirectory(g);
331
+ const baseFwd = baseDir ? normalizeOutputPath(baseDir).replace(/\/+$/, '') : '';
332
+ const rel = relativePattern.replace(/^\//, '');
333
+ // Windows is case-insensitive: compare path casing accordingly so a
334
+ // valid in-root absolute glob is not rejected when its drive/dir
335
+ // casing differs from the resolved root.
336
+ const ci = process.platform === 'win32';
337
+ const baseCmp = ci ? baseFwd.toLowerCase() : baseFwd;
338
+ const rootCmp = ci ? rootFwd.toLowerCase() : rootFwd;
339
+ if (!baseFwd || baseCmp === rootCmp) {
340
+ globPatterns.push(rel);
341
+ } else if (baseCmp.startsWith(rootCmp + '/')) {
342
+ const prefix = baseFwd.slice(rootFwd.length + 1);
343
+ globPatterns.push(prefix ? `${prefix}/${rel}` : rel);
344
+ } else {
345
+ return `Error: absolute glob ${JSON.stringify(g)} resolves outside search root ${JSON.stringify(rootFwd)}; pass a relative glob or move the search path`;
346
+ }
347
+ } else {
348
+ globPatterns.push(g);
349
+ }
350
+ }
351
+ // ripgrep `--glob` uses forward slashes on all platforms; canonicalize
352
+ // `\`→`/` (win32 only) so a `**\*.ts` filter matches instead of being
353
+ // parsed as an escape sequence.
354
+ const normalizedGlobPatterns = uniqueStrings(globPatterns.map(canonicalizeGlobSlashes));
355
+
356
+ const ALLOWED_OUTPUT_MODES = new Set(['files_with_matches', 'content', 'count']);
357
+ const rawOutputMode = typeof args.output_mode === 'string' ? args.output_mode.trim() : '';
358
+ if (rawOutputMode && !ALLOWED_OUTPUT_MODES.has(rawOutputMode)) {
359
+ return `Error: invalid output_mode ${JSON.stringify(args.output_mode)}; expected one of ${[...ALLOWED_OUTPUT_MODES].join(', ')}`;
360
+ }
361
+ // Default to `content` when output_mode is omitted. A pattern is always
362
+ // present here (the no-pattern case returned above), so this is a content
363
+ // search — it should return the matching lines WITH line numbers, not just
364
+ // filenames. Filename-only was forcing callers to re-grep for the actual
365
+ // coordinates (the explorer over-iteration root cause). `files_with_matches`
366
+ // is now opt-in; pure filename discovery belongs to `glob`.
367
+ const outputMode = rawOutputMode || 'content';
368
+ const headLimitRaw = args.head_limit;
369
+ const headLimitCoerced = coerceNonNegInt(headLimitRaw);
370
+ if (Number.isNaN(headLimitCoerced)) {
371
+ return `Error: invalid head_limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
372
+ }
373
+ const headLimit = headLimitCoerced === null
374
+ ? 80
375
+ : (headLimitCoerced === 0 ? Infinity : headLimitCoerced);
376
+ const offsetCoerced = coerceNonNegInt(args.offset);
377
+ if (Number.isNaN(offsetCoerced)) {
378
+ return `Error: invalid offset ${JSON.stringify(args.offset)}; expected a non-negative integer`;
379
+ }
380
+ const offset = offsetCoerced === null || offsetCoerced === 0 ? 0 : offsetCoerced;
381
+ const caseInsensitive = args['-i'] === true;
382
+ const showLineNumbers = args['-n'] !== false;
383
+ const coerceContext = (value) => {
384
+ if (value === undefined || value === null || value === '') return null;
385
+ const n = Number(value);
386
+ if (!Number.isFinite(n) || n < 0) return NaN;
387
+ return Math.min(Math.floor(n), GREP_CONTEXT_MAX);
388
+ };
389
+ let afterN = coerceContext(args['-A']);
390
+ let beforeN = coerceContext(args['-B']);
391
+ let contextN = args['-C'] !== undefined && args['-C'] !== null && args['-C'] !== ''
392
+ ? coerceContext(args['-C'])
393
+ : coerceContext(args.context);
394
+ if (contextN !== null && contextN > 0) {
395
+ if (afterN === 0) afterN = null;
396
+ if (beforeN === 0) beforeN = null;
397
+ }
398
+ for (const [name, value] of [['-A', afterN], ['-B', beforeN], ['-C', contextN]]) {
399
+ if (Number.isNaN(value)) {
400
+ return `Error: invalid context option ${name}; expected a non-negative finite integer`;
401
+ }
402
+ }
403
+ const rawType = args.type;
404
+ let fileType = '';
405
+ let fileTypes = [];
406
+ if (Array.isArray(rawType)) {
407
+ for (const entry of rawType) {
408
+ if (typeof entry !== 'string') {
409
+ return `Error: invalid type entry ${JSON.stringify(entry)}; expected string`;
410
+ }
411
+ const t = entry.trim();
412
+ if (t) fileTypes.push(t);
413
+ }
414
+ } else if (typeof rawType === 'string') {
415
+ const t = rawType.trim();
416
+ if (t) {
417
+ fileTypes = [t];
418
+ fileType = t;
419
+ }
420
+ } else if (rawType !== undefined && rawType !== null) {
421
+ return `Error: invalid type ${JSON.stringify(rawType)}; expected string or string[]`;
422
+ }
423
+ if (fileTypes.length > 1) fileType = fileTypes;
424
+ else if (fileTypes.length === 1) fileType = fileTypes[0];
425
+ const cacheKey = buildGrepCacheKey({
426
+ patterns,
427
+ searchPath: normalizeOutputPath(grepResolvedPath),
428
+ globPatterns: normalizedGlobPatterns,
429
+ outputMode,
430
+ headLimit,
431
+ offset,
432
+ caseInsensitive,
433
+ showLineNumbers,
434
+ beforeN,
435
+ afterN,
436
+ contextN,
437
+ multilineMode,
438
+ onlyMatching: args['-o'] === true,
439
+ fileType,
440
+ });
441
+ // Single-file grep registers a whole-file read snapshot (parity with
442
+ // apply_patch), satisfying the read-before-edit guard while keeping drift
443
+ // detection intact via the auto-computed contentHash. Directory/glob greps
444
+ // do NOT record.
445
+ const recordGrepReadSnapshot = (st) => {
446
+ try {
447
+ if (st && st.isFile()) {
448
+ recordReadSnapshot(grepResolvedPath, st, readStateScope, { source: 'grep' });
449
+ }
450
+ } catch {}
451
+ };
452
+
453
+ const cached = cacheGet(cacheKey);
454
+ // Cache-hit returns a PRIOR grep's output; the file may have changed since
455
+ // that result was cached. Recording a fresh whole-file snapshot here would
456
+ // mismatch what the caller actually saw (stale cached lines) and defeat
457
+ // drift detection. So only the fresh-compute path (below) records a read.
458
+ if (cached !== null) return cached;
459
+
460
+ let grepStat;
461
+ try { grepStat = statSync(grepResolvedPath); }
462
+ catch (err) {
463
+ const msg = `Error: path does not exist: ${normalizeOutputPath(grepResolvedPath)} (${err?.code || 'ENOENT'})`;
464
+ return msg + await _suggestIndexedPaths(grepResolvedPath, executeChildBuiltinTool, workDir);
465
+ }
466
+ const filenameOmitted = grepStat.isFile();
467
+
468
+ // rg builds --glob overrides rooted at its process cwd and relativizes each
469
+ // candidate against it with a CASE-SENSITIVE prefix strip; workDir is
470
+ // case-normalized (lowercased) while callers pass real-cased absolute paths,
471
+ // so the strip fails and slash-anchored globs (src/**/*.mjs) silently match
472
+ // nothing. Spawn rg at the TRUE-CASED search root so relativization — and
473
+ // therefore glob anchoring — always engages. Relative searchPath keeps the
474
+ // workDir cwd (both sides already share workDir's casing).
475
+ let rgSpawnCwd = workDir;
476
+ if (isAbsolute(searchPath)) {
477
+ searchPath = trueCasePath(searchPath);
478
+ if (grepStat.isDirectory()) rgSpawnCwd = searchPath;
479
+ }
480
+
481
+ try {
482
+ const GREP_CONTENT_HARD_CAP = 300;
483
+ const callerExplicitUnlimited = headLimitCoerced === 0;
484
+ const effectiveHeadLimit = headLimit === Infinity
485
+ ? (callerExplicitUnlimited ? Infinity : (outputMode === 'content' ? GREP_CONTENT_HARD_CAP : Infinity))
486
+ : headLimit;
487
+ const rgArgs = buildGrepRgArgs({
488
+ patterns,
489
+ searchPath,
490
+ globPatterns: normalizedGlobPatterns,
491
+ outputMode,
492
+ caseInsensitive,
493
+ showLineNumbers,
494
+ beforeN,
495
+ afterN,
496
+ contextN,
497
+ multilineMode,
498
+ fileType,
499
+ onlyMatching: args['-o'] === true,
500
+ });
501
+ let windowed;
502
+ let totalWindowed = 0;
503
+ let totalKnown = true;
504
+ let rgPartialSuffix = '';
505
+ if (effectiveHeadLimit !== Infinity) {
506
+ const summaryLimit = outputMode === 'content' ? 120 : 0;
507
+ const streamed = await runRgWindowedLines(rgArgs, { cwd: rgSpawnCwd }, {
508
+ offset,
509
+ limit: effectiveHeadLimit,
510
+ summaryLimit,
511
+ });
512
+ windowed = streamed.lines;
513
+ totalWindowed = streamed.totalSeen;
514
+ totalKnown = streamed.complete;
515
+ if (streamed.partial) {
516
+ totalKnown = false;
517
+ rgPartialSuffix = streamed.rgStderr
518
+ ? `\n[warning] rg exit 2 (partial results): ${String(streamed.rgStderr).trim().slice(0, 300)}`
519
+ : '\n[warning] rg exit 2 (partial results)';
520
+ }
521
+ } else {
522
+ const stdout = await runRg(rgArgs, { cwd: rgSpawnCwd });
523
+ const allLines = String(stdout).split('\n').filter(Boolean);
524
+ windowed = offset > 0 ? allLines.slice(offset) : allLines;
525
+ totalWindowed = windowed.length;
526
+ // runRg boxes stdout + sets .truncated when the 20MB stdout cap
527
+ // tripped (rg-runner). Mark the result incomplete so formatGrepOutput
528
+ // emits the truncation notice instead of presenting it as complete.
529
+ if (typeof stdout === 'object' && stdout.truncated) totalKnown = false;
530
+ if (typeof stdout === 'object' && stdout.partial) {
531
+ totalKnown = false;
532
+ rgPartialSuffix = stdout.rgStderr
533
+ ? `\n[warning] rg exit 2 (partial results): ${String(stdout.rgStderr).trim().slice(0, 300)}`
534
+ : '\n[warning] rg exit 2 (partial results)';
535
+ }
536
+ }
537
+ let body = formatGrepOutput({
538
+ windowed,
539
+ totalWindowed,
540
+ totalKnown,
541
+ headLimit,
542
+ offset,
543
+ outputMode,
544
+ patterns,
545
+ beforeN,
546
+ afterN,
547
+ contextN,
548
+ searchPath,
549
+ grepResolvedPath,
550
+ workDir,
551
+ globPatterns: normalizedGlobPatterns,
552
+ fileType,
553
+ filenameOmitted,
554
+ });
555
+ if (!body) {
556
+ const pathInfo = grepStat.isDirectory() ? 'path exists (dir)' : 'path exists (file)';
557
+ const patternStr = patterns.length === 1 ? JSON.stringify(patterns[0]) : JSON.stringify(patterns);
558
+ const globStr = normalizedGlobPatterns.length > 0 ? ` glob=${JSON.stringify(normalizedGlobPatterns)}` : '';
559
+ body = `(no matches) pattern=${patternStr} path=${searchPath}${globStr}; ${pathInfo}`;
560
+ // Cased-letter hint: a no-match single-pattern search whose pattern
561
+ // carries cased letters may have failed only on case. Run ONE
562
+ // case-insensitive probe; if it would match, nudge toward `-i`.
563
+ // Skipped for arrays (single-pattern support is enough) and when
564
+ // `-i` is already set or the pattern has no cased letters. Also
565
+ // require a true zero-match search: an empty body with offset>0 (or
566
+ // pre-offset matches) just means the window skipped past real
567
+ // case-sensitive hits, so the hint would be misleading.
568
+ const trueZeroMatch = offset === 0 && totalWindowed === 0;
569
+ if (trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
570
+ try {
571
+ const probeArgs = buildGrepRgArgs({
572
+ patterns,
573
+ searchPath,
574
+ globPatterns: normalizedGlobPatterns,
575
+ outputMode: 'files_with_matches',
576
+ caseInsensitive: true,
577
+ showLineNumbers: false,
578
+ beforeN: null,
579
+ afterN: null,
580
+ contextN: null,
581
+ multilineMode,
582
+ fileType,
583
+ onlyMatching: false,
584
+ });
585
+ const probeOut = await runRg(probeArgs, { cwd: rgSpawnCwd });
586
+ if (String(probeOut).split('\n').some(Boolean)) {
587
+ body += ' (case-insensitive would match — try -i)';
588
+ }
589
+ } catch { /* best-effort hint */ }
590
+ }
591
+ }
592
+ // Array-pattern visibility: append a per-pattern file-count summary so
593
+ // zero-hit members of the merged result are not silently hidden.
594
+ let perPatternSummary = '';
595
+ if (patterns.length > 1) {
596
+ perPatternSummary = await _perPatternFileCounts({
597
+ patterns,
598
+ searchPath,
599
+ globPatterns: normalizedGlobPatterns,
600
+ caseInsensitive,
601
+ multilineMode,
602
+ fileType,
603
+ workDir: rgSpawnCwd,
604
+ });
605
+ }
606
+ const out = body + rgPartialSuffix + perPatternSummary;
607
+ const shownLines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
608
+ const remaining = Math.max(0, totalWindowed - shownLines.length);
609
+ // Mirrors formatGrepOutput truncation / totalKnown semantics.
610
+ if (options?.scopedCacheOutcome && (!totalKnown || remaining > 0)) {
611
+ markScopedCacheIncomplete(options.scopedCacheOutcome);
612
+ }
613
+ recordGrepReadSnapshot(grepStat);
614
+ if (totalKnown && remaining === 0) {
615
+ cacheSet(cacheKey, out, { scopes: [grepResolvedPath] });
616
+ }
617
+ // ② completion progress (claude "Found N" parity). Best-effort,
618
+ // no-op when onProgress is absent (no progressToken).
619
+ if (typeof options?.onProgress === 'function') {
620
+ try {
621
+ let _n = totalWindowed;
622
+ let _label = 'matches';
623
+ if (outputMode === 'files_with_matches') {
624
+ _label = 'files';
625
+ } else if (outputMode === 'count') {
626
+ _n = 0;
627
+ for (const _line of windowed) { const _c = parseGrepCountLine(_line); if (_c) _n += _c.count; }
628
+ }
629
+ options.onProgress(`found ${_n} ${_label}`);
630
+ } catch { /* best-effort */ }
631
+ }
632
+ return out;
633
+ }
634
+ catch (err) {
635
+ const stderr = err?.stderr ? String(err.stderr).trim() : '';
636
+ const msg = stderr || err?.message || String(err);
637
+ return `Error: ${msg.slice(0, 500)}`;
638
+ }
639
+ }
640
+
641
+ export async function executeGlobTool(args, workDir, options = {}) {
642
+ args = normalizeGlobArgs(args);
643
+ args.path = Array.isArray(args.path)
644
+ ? args.path.map((p) => normalizeInputPath(p)).filter((p) => typeof p === 'string' && p)
645
+ : normalizeInputPath(args.path);
646
+ if (Array.isArray(args.path) && args.path.length === 0) {
647
+ return 'Error: path array must contain at least one base directory';
648
+ }
649
+ args.pattern = coerceShapeFlex(args.pattern);
650
+ const rawPattern = args.pattern;
651
+ // ripgrep `--glob` matchers use forward slashes on all platforms;
652
+ // canonicalize `\`→`/` (win32 only) so a `**\*.ts` pattern matches
653
+ // instead of being parsed as an escape sequence.
654
+ let patterns = uniqueStrings((Array.isArray(rawPattern)
655
+ ? rawPattern.filter(p => typeof p === 'string' && p)
656
+ : (rawPattern ? [String(rawPattern)] : [])).map(normalizeInputPath).map(canonicalizeGlobSlashes));
657
+ if (patterns.length === 0) {
658
+ if (Array.isArray(args.path)) {
659
+ const pathGlobs = args.path.filter((p) => hasGlobMagic(p));
660
+ if (pathGlobs.length > 0 && pathGlobs.length === args.path.length) {
661
+ patterns = uniqueStrings(pathGlobs.map(normalizeInputPath).map(canonicalizeGlobSlashes));
662
+ args.path = undefined;
663
+ }
664
+ } else if (hasGlobMagic(args.path)) {
665
+ patterns = [canonicalizeGlobSlashes(normalizeInputPath(args.path))];
666
+ args.path = undefined;
667
+ }
668
+ }
669
+ if (patterns.length === 0) {
670
+ return globMissingPatternMessage();
671
+ }
672
+
673
+ const basePaths = (Array.isArray(args.path) && args.path.length > 0)
674
+ ? args.path
675
+ : [args.path || '.'];
676
+ // A base path carrying glob magic (path:'src/**/cache/*') names a SET of
677
+ // directories, not a literal one — resolving it literally ENOENTs. Split
678
+ // it the way grep's path handling does: walk from the static baseDir and
679
+ // fold the magic suffix into each pattern under that root.
680
+ const baseEntries = basePaths.map((basePath) => {
681
+ if (typeof basePath !== 'string' || !hasGlobMagic(basePath)) return { root: basePath, prefix: '' };
682
+ const { baseDir, relativePattern } = extractGlobBaseDirectory(canonicalizeGlobSlashes(basePath));
683
+ // A trailing pure-`*` segment ("cache/*") means "the children" — the
684
+ // pattern itself supplies the leaf match, so nesting it one level
685
+ // deeper ("*/<pat>") would skip files directly under the dir. Drop
686
+ // that segment; `**` and mid-path magic still nest.
687
+ const segs = relativePattern.replace(/^\//, '').split('/').filter(Boolean);
688
+ if (segs[segs.length - 1] === '*') segs.pop();
689
+ return { root: baseDir || '.', prefix: segs.join('/') };
690
+ });
691
+ const resolvedSearchRoots = new Map();
692
+ function resolvedForSearchRoot(root) {
693
+ if (!resolvedSearchRoots.has(root)) {
694
+ resolvedSearchRoots.set(root, resolveSearchScope(root, workDir));
695
+ }
696
+ return resolvedSearchRoots.get(root);
697
+ }
698
+ for (const e of baseEntries) {
699
+ if (isUncOrSmbPath(e.root)) {
700
+ return uncRefusalMessage('glob', e.root, e.root);
701
+ }
702
+ const resolvedBase = resolvedForSearchRoot(e.root);
703
+ if (isUncOrSmbPath(resolvedBase)) {
704
+ return uncRefusalMessage('glob', e.root, resolvedBase);
705
+ }
706
+ }
707
+ for (const p of patterns) {
708
+ if (isAbsolute(p) && isUncOrSmbPath(p)) {
709
+ return uncRefusalMessage('glob', p, p);
710
+ }
711
+ }
712
+ const headLimitRaw = args.head_limit;
713
+ const headLimitCoerced = coerceNonNegInt(headLimitRaw);
714
+ if (Number.isNaN(headLimitCoerced)) {
715
+ return `Error: invalid head_limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
716
+ }
717
+ const headLimit = headLimitCoerced === null
718
+ ? 100
719
+ : (headLimitCoerced === 0 ? Infinity : headLimitCoerced);
720
+ const offsetCoerced = coerceNonNegInt(args.offset);
721
+ if (Number.isNaN(offsetCoerced)) {
722
+ return `Error: invalid offset ${JSON.stringify(args.offset)}; expected a non-negative integer`;
723
+ }
724
+ const offset = offsetCoerced === null || offsetCoerced === 0 ? 0 : offsetCoerced;
725
+ // Internal-only ignore extension (see normalizeGlobArgs). Caller (e.g.
726
+ // ai-wrapped-dispatch broad-cwd preflight) appends basename ignore globs
727
+ // so head_limit bounds SOURCE entries rather than artifact noise.
728
+ const extraIgnoreGlobs = Array.isArray(args._extraIgnoreDirs)
729
+ ? args._extraIgnoreDirs.map((name) => `!**/${name}/**`)
730
+ : [];
731
+ const groups = new Map();
732
+ function addToGroup(root, rel) {
733
+ if (!groups.has(root)) groups.set(root, []);
734
+ const rels = groups.get(root);
735
+ if (!rels.includes(rel)) rels.push(rel);
736
+ }
737
+ for (const p of patterns) {
738
+ if (isAbsolute(p)) {
739
+ const { baseDir, relativePattern } = extractGlobBaseDirectory(p);
740
+ addToGroup(baseDir || baseEntries[0]?.root || '.', relativePattern);
741
+ } else {
742
+ for (const e of baseEntries) addToGroup(e.root, e.prefix ? `${e.prefix}/${p}` : p);
743
+ }
744
+ }
745
+
746
+ const cacheBasePath = [...groups.keys()]
747
+ .map((root) => normalizeOutputPath(resolvedForSearchRoot(root)))
748
+ .sort()
749
+ .join('\x01');
750
+ const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs });
751
+ const cached = cacheGet(cacheKey);
752
+ if (cached !== null) return cached;
753
+
754
+ const globGroups = [...groups.entries()];
755
+
756
+ const allFiles = [];
757
+ const rgErrors = [];
758
+ let accumTruncated = false;
759
+ let rgStdoutTruncated = false;
760
+ let rgStdoutPartial = false;
761
+ const accumCap = 50000;
762
+ const groupRuns = await Promise.all(globGroups.map(async ([root, rels]) => {
763
+ const rgArgs = ['--files', '--hidden'];
764
+ for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
765
+ for (const ex of extraIgnoreGlobs) rgArgs.push('--glob', ex);
766
+ for (const rel of rels) rgArgs.push('--glob', rel);
767
+ const rgCwd = resolvedForSearchRoot(root);
768
+ rgArgs.push('.');
769
+ try { statSync(rgCwd); }
770
+ catch (err) {
771
+ return {
772
+ error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})`,
773
+ paths: [],
774
+ stdoutTruncated: false,
775
+ };
776
+ }
777
+ try {
778
+ const stdout = await runRg(rgArgs, { cwd: rgCwd, timeout: 10000 });
779
+ const stdoutTruncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
780
+ const stdoutPartial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
781
+ const paths = [];
782
+ for (const line of String(stdout).split('\n')) {
783
+ const trimmed = line.trim();
784
+ if (!trimmed) continue;
785
+ paths.push(isAbsolute(trimmed) ? trimmed : resolveAgainstCwd(trimmed, rgCwd));
786
+ }
787
+ return { error: null, paths, stdoutTruncated, stdoutPartial };
788
+ } catch (err) {
789
+ const stderr = String(err?.stderr || err?.message || err).trim().split('\n').slice(0, 3).join('; ');
790
+ return {
791
+ error: `rg failed for ${normalizeOutputPath(root)}: ${stderr || 'unknown error'}`,
792
+ paths: [],
793
+ stdoutTruncated: false,
794
+ stdoutPartial: false,
795
+ };
796
+ }
797
+ }));
798
+
799
+ outer: for (const run of groupRuns) {
800
+ if (run.error) {
801
+ rgErrors.push(run.error);
802
+ continue;
803
+ }
804
+ if (run.stdoutTruncated) rgStdoutTruncated = true;
805
+ if (run.stdoutPartial) rgStdoutPartial = true;
806
+ for (const p of run.paths) {
807
+ allFiles.push(p);
808
+ if (allFiles.length >= accumCap) {
809
+ accumTruncated = true;
810
+ break outer;
811
+ }
812
+ }
813
+ }
814
+ if (rgErrors.length > 0 && allFiles.length === 0) {
815
+ return `Error: ${rgErrors.join(' | ').slice(0, 500)}`;
816
+ }
817
+
818
+ const unique = Array.from(new Set(allFiles));
819
+ // Bound the post-rg stat phase: a single hung stat (dead mount /
820
+ // unresponsive network path) must not pin glob until the 600s bridge stall
821
+ // watchdog. Per-stat 5s deadline → a hung entry is treated as stat-failed
822
+ // and dropped, while normal local stats (sub-ms) are unaffected.
823
+ const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
824
+ const withStat = withStatAll.filter((entry) => entry?.stat != null);
825
+ withStat.sort((a, b) => {
826
+ const dm = b.mtime - a.mtime;
827
+ if (dm !== 0) return dm;
828
+ return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
829
+ });
830
+ const totalBeforeOffset = withStat.length;
831
+ const windowed = offset > 0 ? withStat.slice(offset) : withStat;
832
+ const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit)).map((entry) => {
833
+ const abs = entry.full || resolveAgainstCwd(entry.path, workDir);
834
+ return relativeSearchResultPath(abs, workDir);
835
+ });
836
+ const remaining = windowed.length - capped.length;
837
+ const truncSuffix = accumTruncated
838
+ ? '\n... [truncated at accumulation cap (50000)]'
839
+ : (rgStdoutTruncated ? '\n... [truncated at rg stdout cap (20MB); results incomplete]' : '')
840
+ + (rgStdoutPartial ? '\n... [warning] rg exit 2 (partial results); listing may be incomplete' : '');
841
+ const errSuffix = (rgErrors.length > 0 ? `\n... [warning] ${rgErrors.join(' | ')}` : '') + truncSuffix;
842
+ let emptyDiag = '';
843
+ if (capped.length === 0 && rgErrors.length === 0) {
844
+ const patternStr = patterns.length === 1 ? JSON.stringify(patterns[0]) : JSON.stringify(patterns);
845
+ const baseLabel = basePaths.length === 1 ? normalizeOutputPath(basePaths[0]) : `[${basePaths.map(normalizeOutputPath).join(', ')}]`;
846
+ if (totalBeforeOffset > 0 && offset >= totalBeforeOffset) {
847
+ emptyDiag = `(no entries after offset=${offset}; total=${totalBeforeOffset}) pattern=${patternStr} path=${baseLabel}`;
848
+ } else {
849
+ emptyDiag = `(no files found) pattern=${patternStr} path=${baseLabel}; ${basePathDiagnostic(baseEntries.map((e) => e.root), workDir)}`;
850
+ }
851
+ }
852
+ const body = capped.length > 0
853
+ ? `${capped.join('\n')}${remaining > 0 ? `\n... [${remaining} more entries of ${totalBeforeOffset} total — pass offset:${offset + capped.length} to continue]` : ''}${errSuffix}`
854
+ : '';
855
+ const out = body || emptyDiag || '(no files found)';
856
+ if (options?.scopedCacheOutcome && (accumTruncated || rgStdoutTruncated || rgStdoutPartial || remaining > 0)) {
857
+ markScopedCacheIncomplete(options.scopedCacheOutcome);
858
+ }
859
+ const globIncomplete = accumTruncated || rgStdoutTruncated || rgStdoutPartial || remaining > 0;
860
+ if (!globIncomplete) {
861
+ cacheSet(cacheKey, out, { scopes: [...groups.keys()].map((root) => resolvedForSearchRoot(root)) });
862
+ }
863
+ // ② completion progress (claude "Found N" parity). Best-effort, no-op
864
+ // when onProgress is absent (no progressToken).
865
+ if (typeof options?.onProgress === 'function') {
866
+ try { options.onProgress(`found ${totalBeforeOffset} files`); } catch { /* best-effort */ }
867
+ }
868
+ return out;
869
+ }