@moretea-labs/forge 1.8.1 → 1.8.2

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 (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -1,11 +1,9 @@
1
1
  import { createHash } from 'crypto';
2
- import { execFile } from 'child_process';
3
2
  import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs';
4
- import { lstat as lstatAsync, readFile as readFileAsync, readdir as readdirAsync, stat as statAsync } from 'fs/promises';
5
- import { join, resolve } from 'path';
3
+ import { delimiter, join, resolve } from 'path';
6
4
  import { runProcess } from '../../effects/process-runner';
7
- import { globMatches, resolveMcpPath } from '../mcp/paths';
8
- import type { McpPolicy } from '../mcp/types';
5
+ import { globMatches, resolveMcpPath } from '../../../adapters/mcp/paths';
6
+ import type { McpPolicy } from '../../../adapters/mcp/types';
9
7
  import {
10
8
  collectSessionIdentity,
11
9
  getOrCreateSessionCache,
@@ -306,28 +304,6 @@ function gitSearchFileInventory(repoRoot: string): string[] | null {
306
304
  }
307
305
  }
308
306
 
309
- function gitSearchFileInventoryAsync(repoRoot: string): Promise<string[] | null> {
310
- return new Promise((resolve) => {
311
- execFile(
312
- 'git',
313
- ['ls-files', '-z', '--cached', '--others', '--exclude-standard'],
314
- {
315
- cwd: repoRoot,
316
- timeout: 10_000,
317
- maxBuffer: SEARCH_GIT_INVENTORY_MAX_OUTPUT_BYTES,
318
- encoding: 'utf8',
319
- },
320
- (error, stdout) => {
321
- if (error || typeof stdout !== 'string') {
322
- resolve(null);
323
- return;
324
- }
325
- resolve(parseGitSearchFileInventory(stdout));
326
- },
327
- );
328
- });
329
- }
330
-
331
307
  function binary(bytes: Buffer): boolean {
332
308
  return bytes.subarray(0, Math.min(bytes.length, 8000)).includes(0);
333
309
  }
@@ -352,36 +328,6 @@ function walk(repoRoot: string, root: string, maxFiles: number, excludes: string
352
328
  }
353
329
  }
354
330
 
355
- async function walkAsync(repoRoot: string, root: string, maxFiles: number, excludes: string[], output: string[]): Promise<void> {
356
- if (output.length >= maxFiles) return;
357
- const absolute = join(repoRoot, root);
358
- let info;
359
- try {
360
- info = await lstatAsync(absolute);
361
- } catch {
362
- return;
363
- }
364
- if (info.isSymbolicLink()) return;
365
- if (info.isFile()) {
366
- output.push(root);
367
- return;
368
- }
369
- if (!info.isDirectory()) return;
370
- let entries;
371
- try {
372
- entries = await readdirAsync(absolute, { withFileTypes: true });
373
- } catch {
374
- return;
375
- }
376
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
377
- if (output.length >= maxFiles) break;
378
- const child = root ? `${root}/${entry.name}` : entry.name;
379
- if (isExcluded(child, excludes) || isExcluded(`${child}/`, excludes)) continue;
380
- if (entry.isDirectory()) await walkAsync(repoRoot, child, maxFiles, excludes, output);
381
- else if (entry.isFile()) output.push(child);
382
- }
383
- }
384
-
385
331
  export interface SearchRepositoryOptions {
386
332
  query: string;
387
333
  includeGlobs?: string[];
@@ -499,7 +445,6 @@ function searchInventory(
499
445
  repoRoot: string,
500
446
  includes: string[],
501
447
  excludes: string[],
502
- maxFiles: number,
503
448
  cacheKey?: string,
504
449
  ): { files: string[]; candidateCount: number; cacheHit: boolean } {
505
450
  const inventoryKey = JSON.stringify({ repoRoot, excludes, cacheKey });
@@ -519,36 +464,10 @@ function searchInventory(
519
464
  return files;
520
465
  })();
521
466
  const candidates = inventory.filter((path) => isIncluded(path, includes) && !isExcluded(path, excludes));
522
- return { files: candidates.slice(0, maxFiles), candidateCount: candidates.length, cacheHit };
523
- }
524
-
525
- async function searchInventoryAsync(
526
- repoRoot: string,
527
- includes: string[],
528
- excludes: string[],
529
- maxFiles: number,
530
- cacheKey?: string,
531
- ): Promise<{ files: string[]; candidateCount: number; cacheHit: boolean }> {
532
- const inventoryKey = JSON.stringify({ repoRoot, excludes, cacheKey });
533
- const now = Date.now();
534
- const cachedInventory = searchFileInventoryCache.get(inventoryKey);
535
- const cacheHit = Boolean(cachedInventory && now - cachedInventory.createdAt < SEARCH_FILE_INVENTORY_CACHE_TTL_MS);
536
- let inventory: string[];
537
- if (cacheHit) {
538
- inventory = cachedInventory!.files;
539
- } else {
540
- const gitFiles = await gitSearchFileInventoryAsync(repoRoot);
541
- if (gitFiles) {
542
- inventory = gitFiles;
543
- } else {
544
- const walked: string[] = [];
545
- await walkAsync(repoRoot, '', 20_000, excludes, walked);
546
- inventory = walked;
547
- }
548
- searchFileInventoryCache.set(inventoryKey, { createdAt: now, files: inventory });
549
- }
550
- const candidates = inventory.filter((path) => isIncluded(path, includes) && !isExcluded(path, excludes));
551
- return { files: candidates.slice(0, maxFiles), candidateCount: candidates.length, cacheHit };
467
+ // A lexical hit beyond the first N alphabetic Git paths is not optional.
468
+ // The bounded rg matcher owns result limits; inventory must retain all
469
+ // eligible source paths rather than silently losing the rest of the repo.
470
+ return { files: candidates, candidateCount: candidates.length, cacheHit };
552
471
  }
553
472
 
554
473
  interface SearchManyMatchState {
@@ -607,29 +526,17 @@ function exactCodeNeedleMatches(haystack: string, needle: string): boolean {
607
526
  return new RegExp(`(?:^|[^\\p{L}\\p{N}_$])${escaped}(?=$|[^\\p{L}\\p{N}_$])`, 'u').test(haystack);
608
527
  }
609
528
 
610
- function matchSearchManySource(state: SearchManyMatchState, path: string, bytes: Buffer): void {
611
- const raw = bytes.toString('utf-8');
612
- const searchable = state.caseSensitive ? raw : raw.toLowerCase();
613
- const activeNeedles = state.needles.filter(({ query, needle }) =>
614
- (state.counts.get(query) ?? 0) < state.maxResultsPerQuery
615
- && (state.requiredQueries.includes(query)
616
- ? exactCodeNeedleMatches(searchable, needle)
617
- : searchable.includes(needle)));
618
- if (activeNeedles.length === 0) return;
619
-
620
- const lines = raw.split(/\r?\n/);
621
- for (let index = 0; index < lines.length; index += 1) {
622
- const text = lines[index]!;
623
- const haystack = state.caseSensitive ? text : text.toLowerCase();
624
- for (const { query, needle } of activeNeedles) {
625
- const matches = state.requiredQueries.includes(query)
626
- ? exactCodeNeedleMatches(haystack, needle)
627
- : haystack.includes(needle);
628
- if ((state.counts.get(query) ?? 0) >= state.maxResultsPerQuery || !matches) continue;
629
- state.results.push({ query, path, line: index + 1, text: text.slice(0, 500) });
630
- state.matchedPaths.add(path);
631
- state.counts.set(query, (state.counts.get(query) ?? 0) + 1);
632
- }
529
+ function matchSearchManyLine(state: SearchManyMatchState, path: string, line: number, text: string): void {
530
+ const haystack = state.caseSensitive ? text : text.toLowerCase();
531
+ for (const { query, needle } of state.needles) {
532
+ if ((state.counts.get(query) ?? 0) >= state.maxResultsPerQuery) continue;
533
+ const matches = state.requiredQueries.includes(query)
534
+ ? exactCodeNeedleMatches(haystack, needle)
535
+ : haystack.includes(needle);
536
+ if (!matches) continue;
537
+ state.results.push({ query, path, line, text: text.slice(0, 500) });
538
+ state.matchedPaths.add(path);
539
+ state.counts.set(query, (state.counts.get(query) ?? 0) + 1);
633
540
  }
634
541
  }
635
542
 
@@ -674,9 +581,89 @@ function finalizeSearchManyResult(input: {
674
581
  }
675
582
 
676
583
  /**
677
- * Internal Context Plane batch search. Every candidate file is policy-checked
678
- * and read at most once while all lexical terms are matched in the same pass.
679
- * The public search_repository tool remains the simpler single-query primitive.
584
+ * ripgrep owns literal matching and returns line-addressed evidence directly.
585
+ * The caller supplies only policy-checked paths; neither a repository-wide
586
+ * scan nor a second JavaScript file-content scan is permitted.
587
+ */
588
+ function ripgrepCandidateLines(
589
+ repoRoot: string,
590
+ allowedPaths: readonly string[],
591
+ queries: readonly string[],
592
+ caseSensitive: boolean,
593
+ maxResultsPerQuery: number,
594
+ skippedBinaryPaths?: Set<string>,
595
+ ): Map<string, Array<{ line: number; text: string }>> {
596
+ const found = new Map<string, Array<{ line: number; text: string }>>();
597
+ const searchDeadline = Date.now() + 15_000;
598
+ for (let offset = 0; offset < allowedPaths.length; offset += 128) {
599
+ const paths = allowedPaths.slice(offset, offset + 128);
600
+ const remainingMs = searchDeadline - Date.now();
601
+ if (remainingMs <= 0) throw new Error('RIPGREP_SEARCH_TIMEOUT: source discovery exceeded 15s total budget');
602
+ const result = runProcess('rg', [
603
+ '--null', '--with-filename', '--line-number', '--no-heading', '--color', 'never', '--fixed-strings', '--no-ignore', '--no-config', '--no-messages',
604
+ '--max-count', String(Math.min(Math.max(maxResultsPerQuery, 32), 500)),
605
+ ...(caseSensitive ? [] : ['--ignore-case']),
606
+ ...queries.flatMap((query) => ['-e', query]),
607
+ '--', ...paths,
608
+ ], {
609
+ cwd: repoRoot,
610
+ // launchd/systemd often omit package-manager bins from PATH. A missing
611
+ // executable must never masquerade as rg exit 1 (no matches).
612
+ env: { PATH: [process.env.PATH, ...(process.platform === 'darwin' ? ['/opt/homebrew/bin', '/usr/local/bin'] : [])].filter(Boolean).join(delimiter) },
613
+ timeoutMs: Math.min(remainingMs, 5_000),
614
+ maxOutputBytes: 16 * 1024 * 1024,
615
+ });
616
+ if (result.error || (result.status !== 0 && result.status !== 1) || result.timedOut
617
+ || result.stdout.endsWith('\n[output truncated after 16777216 bytes]')) {
618
+ throw new Error('RIPGREP_SEARCH_FAILED: ' + (result.error || result.stderr || String(result.status)));
619
+ }
620
+ // NUL-framed paths survive the shared process redactor. JSON-lines did
621
+ // not: masking source text inside a JSON string invalidated its syntax.
622
+ let cursor = 0;
623
+ while (cursor < result.stdout.length) {
624
+ const filenameEnd = result.stdout.indexOf('\0', cursor);
625
+ const firstLineEnd = result.stdout.indexOf('\n', cursor);
626
+ // Ripgrep prints a plain-text "path: binary file matches" notice when
627
+ // a candidate contains NUL bytes. This notice has no NUL-framed path
628
+ // or line number; discard only notices naming an exact authorized path.
629
+ // One binary match must not invalidate unrelated valid source evidence.
630
+ if (firstLineEnd >= cursor && (filenameEnd < 0 || firstLineEnd < filenameEnd)) {
631
+ const notice = result.stdout.slice(cursor, firstLineEnd);
632
+ const marker = ': binary file matches';
633
+ const markerAt = notice.lastIndexOf(marker);
634
+ const binaryPath = markerAt > 0 ? notice.slice(0, markerAt) : '';
635
+ const suffix = markerAt >= 0 ? notice.slice(markerAt + marker.length) : '';
636
+ if (!paths.includes(binaryPath) || (suffix !== '' && !/^ \(found "\\0" byte around offset \d+\)$/.test(suffix))) {
637
+ throw new Error('RIPGREP_SEARCH_INVALID_OUTPUT: malformed NUL/line-framed match');
638
+ }
639
+ skippedBinaryPaths?.add(binaryPath);
640
+ cursor = firstLineEnd + 1;
641
+ continue;
642
+ }
643
+ const lineEnd = result.stdout.indexOf('\n', filenameEnd + 1);
644
+ const numberEnd = result.stdout.indexOf(':', filenameEnd + 1);
645
+ if (filenameEnd < cursor || lineEnd < 0 || numberEnd <= filenameEnd || numberEnd >= lineEnd) {
646
+ throw new Error('RIPGREP_SEARCH_INVALID_OUTPUT: malformed NUL/line-framed match');
647
+ }
648
+ const path = result.stdout.slice(cursor, filenameEnd);
649
+ const line = Number(result.stdout.slice(filenameEnd + 1, numberEnd));
650
+ if (!Number.isSafeInteger(line) || line <= 0) {
651
+ throw new Error('RIPGREP_SEARCH_INVALID_OUTPUT: malformed source line number');
652
+ }
653
+ const text = result.stdout.slice(numberEnd + 1, lineEnd).replace(/\r$/, '');
654
+ const lines = found.get(path) ?? [];
655
+ lines.push({ line, text });
656
+ found.set(path, lines);
657
+ cursor = lineEnd + 1;
658
+ }
659
+ }
660
+ for (const lines of found.values()) lines.sort((a, b) => a.line - b.line);
661
+ return found;
662
+ }
663
+
664
+ /**
665
+ * Internal Context Plane discovery: ripgrep selects matching authorized files,
666
+ * then bounded source materialization owns line-level evidence and ranking.
680
667
  */
681
668
  export function searchRepositoryMany(
682
669
  repoRoot: string,
@@ -704,13 +691,9 @@ export function searchRepositoryMany(
704
691
  return { ...(cached.result as SearchRepositoryManyResult), cacheHit: true };
705
692
  }
706
693
  }
707
- const inventory = directCandidates && directCandidates.length > 0
708
- ? {
709
- files: directCandidates.slice(0, maxFiles),
710
- candidateCount: directCandidates.length,
711
- cacheHit: false,
712
- }
713
- : searchInventory(repoRoot, includes, excludes, maxFiles, opts.cacheKey);
694
+ const inventory = directCandidates !== undefined
695
+ ? { files: directCandidates.slice(0, maxFiles), candidateCount: directCandidates.length, cacheHit: false }
696
+ : searchInventory(repoRoot, includes, excludes, opts.cacheKey);
714
697
  const matchState = createSearchManyMatchState(
715
698
  queries,
716
699
  maxResultsPerQuery,
@@ -723,30 +706,74 @@ export function searchRepositoryMany(
723
706
  let scannedFiles = 0;
724
707
  let policyDeniedFiles = 0;
725
708
  let skippedLargeFiles = 0;
726
- let skippedBinaryFiles = 0;
709
+ const skippedBinaryPaths = new Set<string>();
727
710
  const canonicalRepoRoot = realpathSync(repoRoot);
728
711
 
712
+ const authorizedCandidatePaths: string[] = [];
713
+ // No external search process may inspect a file before MCP read authorization.
729
714
  for (const path of inventory.files) {
730
- if (searchManyComplete(matchState)) break;
731
715
  const decision = resolveMcpPath(repoRoot, path, policy, 'read', canonicalRepoRoot);
732
- if (!decision.ok || !decision.absolutePath) {
733
- policyDeniedFiles += 1;
734
- continue;
735
- }
716
+ if (!decision.ok || !decision.absolutePath) { policyDeniedFiles += 1; continue; }
736
717
  const info = statSync(decision.absolutePath);
737
718
  if (!info.isFile()) continue;
738
- const size = info.size;
739
- if (size > policy.maxFileBytes) {
740
- skippedLargeFiles += 1;
741
- continue;
742
- }
743
- const bytes = readFileSync(decision.absolutePath);
744
- if (binary(bytes)) {
745
- skippedBinaryFiles += 1;
746
- continue;
719
+ if (info.size > policy.maxFileBytes) { skippedLargeFiles += 1; continue; }
720
+ authorizedCandidatePaths.push(path);
721
+ }
722
+ const exactHits = authorizedCandidatePaths.length && requiredQueries.length
723
+ ? ripgrepCandidateLines(repoRoot, authorizedCandidatePaths, requiredQueries, opts.caseSensitive === true, maxResultsPerQuery, skippedBinaryPaths)
724
+ : null;
725
+ const hasRequiredHit = (needle: string): boolean => exactHits !== null
726
+ && [...exactHits.values()].some((lines) => lines.some(({ text }) => exactCodeNeedleMatches(
727
+ opts.caseSensitive ? text : text.toLowerCase(),
728
+ opts.caseSensitive ? needle : needle.toLowerCase(),
729
+ )));
730
+ // A complete exact-symbol answer needs no broad recall-token pass. If an
731
+ // explicitly requested symbol is absent, fall back to ordinary discovery.
732
+ const hitsByPath = exactHits && requiredQueries.every(hasRequiredHit)
733
+ ? exactHits
734
+ : authorizedCandidatePaths.length
735
+ ? ripgrepCandidateLines(repoRoot, authorizedCandidatePaths, queries, opts.caseSensitive === true, maxResultsPerQuery, skippedBinaryPaths)
736
+ : new Map<string, Array<{ line: number; text: string }>>();
737
+ // Exact symbol needles must not lose to broad recall-token matches that
738
+ // occur in earlier alphabetic paths.
739
+ const exactHit = (path: string): boolean => requiredQueries.some((needle) =>
740
+ (hitsByPath.get(path) ?? []).some((hit) => exactCodeNeedleMatches(
741
+ opts.caseSensitive ? hit.text : hit.text.toLowerCase(),
742
+ opts.caseSensitive ? needle : needle.toLowerCase(),
743
+ )));
744
+ // Prefer the public source owner over same-named local wrappers. A bare
745
+ // declaration and an exported declaration are both exact hits, but they
746
+ // are not equally useful when only a few source files can be returned.
747
+ const declarationRankCache = new Map<string, number>();
748
+ const declarationRank = (path: string): number => {
749
+ const cached = declarationRankCache.get(path);
750
+ if (cached !== undefined) return cached;
751
+ let rank = 0;
752
+ for (const hit of hitsByPath.get(path) ?? []) {
753
+ const source = hit.text.trimStart();
754
+ const declared = /^(?:(?:export|declare|default|async)\s+)*(?:function|class|interface|type|const|let|enum)\s+([\w$]+)/.exec(source)?.[1];
755
+ if (declared === undefined || !requiredQueries.some((needle) =>
756
+ opts.caseSensitive ? declared === needle : declared.toLowerCase() === needle.toLowerCase())) continue;
757
+ rank = Math.max(rank, source.startsWith('export ') ? 2 : 1);
758
+ if (rank === 2) break;
747
759
  }
748
- scannedFiles += 1;
749
- matchSearchManySource(matchState, path, bytes);
760
+ declarationRankCache.set(path, rank);
761
+ return rank;
762
+ };
763
+ // Sorting only paths with evidence preserves the authorized inventory order
764
+ // while avoiding comparisons across thousands of guaranteed no-hit files.
765
+ const pathsWithHits = authorizedCandidatePaths.filter((path) => hitsByPath.has(path));
766
+ const orderedPaths = requiredQueries.length
767
+ ? pathsWithHits.sort((left, right) =>
768
+ declarationRank(right) - declarationRank(left)
769
+ || Number(exactHit(right)) - Number(exactHit(left)))
770
+ : pathsWithHits;
771
+ for (const path of orderedPaths) {
772
+ if (searchManyComplete(matchState) || matchState.matchedPaths.size >= maxFiles) break;
773
+ const hits = hitsByPath.get(path);
774
+ if (!hits) continue;
775
+ for (const hit of hits) matchSearchManyLine(matchState, path, hit.line, hit.text);
776
+ if (matchState.matchedPaths.has(path)) scannedFiles += 1;
750
777
  }
751
778
 
752
779
  // Consumers may bound distinct candidate files after this call. Preserve the
@@ -757,7 +784,7 @@ export function searchRepositoryMany(
757
784
  scannedFiles,
758
785
  policyDeniedFiles,
759
786
  skippedLargeFiles,
760
- skippedBinaryFiles,
787
+ skippedBinaryFiles: skippedBinaryPaths.size,
761
788
  candidateCount: inventory.candidateCount,
762
789
  inventoryFileCount: inventory.files.length,
763
790
  inventoryCacheHit: inventory.cacheHit,
@@ -773,138 +800,20 @@ export function searchRepositoryMany(
773
800
  return payload;
774
801
  }
775
802
 
776
- // File reads are already bounded by policy/maxFiles. A wider batch keeps broad
777
- // Context scans from serializing dozens of filesystem waves while still
778
- // preserving original-order matching and early-stop semantics below.
779
- const ASYNC_SEARCH_READ_BATCH_SIZE = 64;
780
-
781
- type AsyncSearchCandidate =
782
- | { path: string; kind: 'denied' }
783
- | { path: string; kind: 'large' }
784
- | { path: string; kind: 'binary' }
785
- | { path: string; kind: 'nonfile' }
786
- | { path: string; kind: 'source'; bytes: Buffer }
787
- | { path: string; kind: 'error'; error: unknown };
788
-
789
- async function readSearchCandidateAsync(
790
- repoRoot: string,
791
- policy: McpPolicy,
792
- canonicalRepoRoot: string,
793
- path: string,
794
- ): Promise<AsyncSearchCandidate> {
795
- try {
796
- const decision = resolveMcpPath(repoRoot, path, policy, 'read', canonicalRepoRoot);
797
- if (!decision.ok || !decision.absolutePath) return { path, kind: 'denied' };
798
- const info = await statAsync(decision.absolutePath);
799
- if (!info.isFile()) return { path, kind: 'nonfile' };
800
- const size = info.size;
801
- if (size > policy.maxFileBytes) return { path, kind: 'large' };
802
- const bytes = await readFileAsync(decision.absolutePath);
803
- if (binary(bytes)) return { path, kind: 'binary' };
804
- return { path, kind: 'source', bytes };
805
- } catch (error) {
806
- return { path, kind: 'error', error };
807
- }
808
- }
809
-
810
803
  /**
811
- * Async Context Plane batch search with result semantics matching
812
- * searchRepositoryMany. Candidate reads happen in bounded batches, while policy
813
- * accounting, early-stop behavior, matching, and result ordering are consumed in
814
- * original repository order on the caller thread.
804
+ * Async Context Pack uses the same canonical ripgrep-backed search implementation.
805
+ * A second filesystem scanning implementation would diverge in policy, matching
806
+ * and ranking; this short-lived sidecar does not own a separate search engine.
815
807
  */
816
808
  export async function searchRepositoryManyAsync(
817
809
  repoRoot: string,
818
810
  policy: McpPolicy,
819
811
  opts: SearchRepositoryManyOptions,
820
812
  ): Promise<SearchRepositoryManyResult> {
821
- const {
822
- queries,
823
- maxResultsPerQuery,
824
- maxFiles,
825
- includes,
826
- excludes,
827
- directCandidates,
828
- completionMode,
829
- discoveryTargetFiles,
830
- discoveryMinQueryCoverage,
831
- requiredQueries,
832
- includeKey,
833
- batchQueryKey,
834
- } = searchRepositoryManyCacheIdentity(opts);
835
- const sessionCache = bindSessionCache(repoRoot, opts.session);
836
- if (sessionCache) {
837
- const cached = sessionCache.getSearch(batchQueryKey, includeKey);
838
- if (cached?.result && typeof cached.result === 'object') {
839
- return { ...(cached.result as SearchRepositoryManyResult), cacheHit: true };
840
- }
841
- }
842
- const inventory = directCandidates && directCandidates.length > 0
843
- ? {
844
- files: directCandidates.slice(0, maxFiles),
845
- candidateCount: directCandidates.length,
846
- cacheHit: false,
847
- }
848
- : await searchInventoryAsync(repoRoot, includes, excludes, maxFiles, opts.cacheKey);
849
- const matchState = createSearchManyMatchState(
850
- queries,
851
- maxResultsPerQuery,
852
- completionMode,
853
- discoveryTargetFiles,
854
- discoveryMinQueryCoverage,
855
- requiredQueries,
856
- opts.caseSensitive === true,
857
- );
858
- let scannedFiles = 0;
859
- let policyDeniedFiles = 0;
860
- let skippedLargeFiles = 0;
861
- let skippedBinaryFiles = 0;
862
- const canonicalRepoRoot = realpathSync(repoRoot);
863
- for (let offset = 0; offset < inventory.files.length && !searchManyComplete(matchState); offset += ASYNC_SEARCH_READ_BATCH_SIZE) {
864
- const batchPaths = inventory.files.slice(offset, offset + ASYNC_SEARCH_READ_BATCH_SIZE);
865
- const batch = await Promise.all(batchPaths.map((path) => readSearchCandidateAsync(repoRoot, policy, canonicalRepoRoot, path)));
866
- for (const candidate of batch) {
867
- if (searchManyComplete(matchState)) break;
868
- if (candidate.kind === 'denied') {
869
- policyDeniedFiles += 1;
870
- continue;
871
- }
872
- if (candidate.kind === 'large') {
873
- skippedLargeFiles += 1;
874
- continue;
875
- }
876
- if (candidate.kind === 'binary') {
877
- skippedBinaryFiles += 1;
878
- continue;
879
- }
880
- if (candidate.kind === 'nonfile') continue;
881
- if (candidate.kind === 'error') throw candidate.error;
882
- scannedFiles += 1;
883
- matchSearchManySource(matchState, candidate.path, candidate.bytes);
884
- }
885
- }
886
-
887
- const payload = finalizeSearchManyResult({
888
- state: matchState,
889
- scannedFiles,
890
- policyDeniedFiles,
891
- skippedLargeFiles,
892
- skippedBinaryFiles,
893
- candidateCount: inventory.candidateCount,
894
- inventoryFileCount: inventory.files.length,
895
- inventoryCacheHit: inventory.cacheHit,
896
- });
897
- if (sessionCache) {
898
- sessionCache.putSearch({
899
- query: batchQueryKey,
900
- includeKey,
901
- result: payload,
902
- scannedFiles,
903
- });
904
- }
905
- return payload;
813
+ return searchRepositoryMany(repoRoot, policy, opts);
906
814
  }
907
815
 
816
+ /** Single-query facade delegates to the canonical policy-checked rg engine. */
908
817
  export function searchRepository(repoRoot: string, policy: McpPolicy, opts: SearchRepositoryOptions & {
909
818
  session?: RepositoryReadSession;
910
819
  }): {
@@ -915,92 +824,25 @@ export function searchRepository(repoRoot: string, policy: McpPolicy, opts: Sear
915
824
  skippedLargeFiles: number;
916
825
  skippedBinaryFiles: number;
917
826
  truncated: boolean;
918
- truncationReason?: "max_results" | "max_files";
827
+ truncationReason?: 'max_results' | 'max_files';
919
828
  cacheHit?: boolean;
920
829
  } {
921
- const query = opts.query;
922
- if (!query.trim()) throw new Error('search query is required');
923
- const maxResults = Math.min(Math.max(opts.maxResults ?? 100, 1), 500);
924
- const maxFiles = Math.min(Math.max(opts.maxFiles ?? 5000, 1), 20_000);
925
- const includes = opts.includeGlobs ?? [];
926
- // Explicit include globs opt into a targeted scan; MCP read policy remains the single authority.
927
- const excludes = [...(includes.length === 0 ? DEFAULT_EXCLUDES : []), ...(opts.excludeGlobs ?? [])];
928
- const includeKey = JSON.stringify({
929
- includes,
930
- excludes,
931
- maxResults,
932
- maxFiles,
933
- caseSensitive: opts.caseSensitive === true,
830
+ const results = searchRepositoryMany(repoRoot, policy, {
831
+ queries: [opts.query],
832
+ includeGlobs: opts.includeGlobs,
833
+ excludeGlobs: opts.excludeGlobs,
834
+ maxResultsPerQuery: opts.maxResults,
835
+ maxFiles: opts.maxFiles,
836
+ caseSensitive: opts.caseSensitive,
934
837
  cacheKey: opts.cacheKey,
838
+ session: opts.session,
935
839
  });
936
- const sessionCache = bindSessionCache(repoRoot, opts.session);
937
- if (sessionCache) {
938
- const cached = sessionCache.getSearch(query, includeKey);
939
- if (cached && cached.result && typeof cached.result === 'object') {
940
- return { ...(cached.result as ReturnType<typeof searchRepository>), cacheHit: true };
941
- }
942
- }
943
- const inventory = searchInventory(repoRoot, includes, excludes, maxFiles, opts.cacheKey);
944
- const files = inventory.files;
945
- const needle = opts.caseSensitive ? query : query.toLowerCase();
946
- const results: Array<{ path: string; line: number; text: string }> = [];
947
- let scannedFiles = 0;
948
- let policyDeniedFiles = 0;
949
- let skippedLargeFiles = 0;
950
- let skippedBinaryFiles = 0;
951
- for (const path of files) {
952
- if (results.length >= maxResults) break;
953
- if (!isIncluded(path, includes) || isExcluded(path, excludes)) continue;
954
- const decision = resolveMcpPath(repoRoot, path, policy, 'read');
955
- if (!decision.ok || !decision.absolutePath) {
956
- policyDeniedFiles += 1;
957
- continue;
958
- }
959
- const info = statSync(decision.absolutePath);
960
- if (!info.isFile()) continue;
961
- const size = info.size;
962
- if (size > policy.maxFileBytes) {
963
- skippedLargeFiles += 1;
964
- continue;
965
- }
966
- const bytes = readFileSync(decision.absolutePath);
967
- if (binary(bytes)) {
968
- skippedBinaryFiles += 1;
969
- continue;
970
- }
971
- scannedFiles += 1;
972
- const raw = bytes.toString('utf-8');
973
- const lines = raw.split(/\r?\n/);
974
- for (let index = 0; index < lines.length && results.length < maxResults; index += 1) {
975
- const haystack = opts.caseSensitive ? lines[index] : lines[index].toLowerCase();
976
- if (haystack.includes(needle)) results.push({ path, line: index + 1, text: lines[index].slice(0, 500) });
977
- }
978
- }
979
- const truncationReason = results.length >= maxResults
980
- ? 'max_results' as const
981
- : inventory.candidateCount > files.length
982
- ? 'max_files' as const
983
- : undefined;
984
- const payload = {
985
- query,
986
- results,
987
- scannedFiles,
988
- policyDeniedFiles,
989
- skippedLargeFiles,
990
- skippedBinaryFiles,
991
- truncated: Boolean(truncationReason),
992
- truncationReason,
993
- cacheHit: inventory.cacheHit,
840
+ return {
841
+ ...results,
842
+ query: opts.query,
843
+ results: results.results.map(({ path, line, text }) => ({ path, line, text })),
844
+ truncationReason: results.truncationReason === 'discovery_budget' ? undefined : results.truncationReason,
994
845
  };
995
- if (sessionCache) {
996
- sessionCache.putSearch({
997
- query,
998
- includeKey,
999
- result: payload,
1000
- scannedFiles,
1001
- });
1002
- }
1003
- return payload;
1004
846
  }
1005
847
 
1006
848
  export function clearSearchFileInventoryCacheForTest(): void {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, statSync } from "fs";
2
2
  import { delimiter, extname, isAbsolute, join } from "path";
3
3
  import { spawnSync } from "child_process";
4
- import { redactSensitiveText } from "../runtime/evidence/sensitive-output";
4
+ import { redactSensitiveText, redactSensitiveValue } from "../runtime/evidence/sensitive-output";
5
5
 
6
6
  export interface ProcessOutputRedaction {
7
7
  readonly pattern: RegExp;
@@ -53,6 +53,22 @@ export function redactProcessOutput(
53
53
  value: string,
54
54
  redactions: readonly ProcessOutputRedaction[] = [],
55
55
  ): string {
56
+ // CLI JSON is a structured protocol, not free-form log text. Regex redaction
57
+ // can replace an array/object value with a bare [REDACTED] marker, destroying
58
+ // the JSON and causing an otherwise successful CoreDevice action to fail.
59
+ // Redact parsed values first, then reserialize so secrets stay hidden and
60
+ // downstream machine-readable contracts remain valid.
61
+ if (redactions.length === 0) {
62
+ try {
63
+ const parsed = JSON.parse(value);
64
+ if (parsed !== null && typeof parsed === "object") {
65
+ const structured = redactSensitiveValue(parsed);
66
+ if (structured.changed) return JSON.stringify(structured.value);
67
+ }
68
+ } catch {
69
+ // Ordinary command output is text, not necessarily JSON.
70
+ }
71
+ }
56
72
  const customRedacted = redactions.reduce(
57
73
  (current, redaction) => current.replace(redaction.pattern, redaction.replacement),
58
74
  value,
@@ -266,7 +266,7 @@ export class GenericLspSemanticProvider implements SemanticNavigationProvider {
266
266
  return requests.map(() => ({
267
267
  ok: false,
268
268
  code: 'LSP_SEMANTIC_READ_SCOPE_UNSUPPORTED',
269
- message: `External ${this.descriptor.id} navigation is restricted to the controller read profile; use lexical/CodeGraph evidence under narrower read policies.`,
269
+ message: `External ${this.descriptor.id} navigation is restricted to the controller read profile; use bounded lexical/raw source evidence under narrower read policies.`,
270
270
  }));
271
271
  }
272
272
  const outcomes: Array<SemanticNavigationOutcome | undefined> = new Array(requests.length);