@moretea-labs/forge 1.6.0 → 1.7.0

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 (396) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -0
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +1 -1
  6. package/SKILL.md +2 -2
  7. package/THIRD_PARTY_NOTICES.md +1 -1
  8. package/agents/openai.yaml +1 -1
  9. package/assets/hooks/hook-input.sh +11 -8
  10. package/assets/hooks/lib/session-state.sh +10 -7
  11. package/assets/hooks/lib/workflow-state.sh +13 -177
  12. package/assets/hooks/post-tool-observer.sh +8 -2
  13. package/assets/hooks/prompt-guard.sh +5 -81
  14. package/assets/hooks/session-start-context.sh +56 -1
  15. package/assets/partials/05-workflow.partial.md +1 -1
  16. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  17. package/assets/plugin-registry.v1.json +1 -1
  18. package/assets/reference-configs/document-generation.md +1 -1
  19. package/assets/reference-configs/harness-overview.md +1 -1
  20. package/assets/reference-configs/sprint-contracts.md +3 -3
  21. package/assets/skill-version.json +2 -2
  22. package/assets/templates/contract.template.md +2 -7
  23. package/assets/templates/helpers/capture-plan.sh +2 -2
  24. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  25. package/assets/templates/helpers/contract-worktree.sh +1 -21
  26. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  27. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  28. package/assets/templates/helpers/new-plan.sh +1 -1
  29. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  30. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  31. package/assets/templates/helpers/verify-contract.sh +1 -111
  32. package/assets/templates/helpers/verify-sprint.sh +3 -180
  33. package/assets/templates/plan.template.md +1 -1
  34. package/assets/templates/review.template.md +9 -34
  35. package/assets/workflow-contract.v1.json +10 -19
  36. package/docs/README.md +10 -10
  37. package/docs/architecture/CURRENT.md +170 -0
  38. package/docs/architecture/EVOLUTION.md +42 -0
  39. package/docs/architecture/current/README.md +4 -156
  40. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  41. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  42. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  43. package/docs/architecture/history/README.md +7 -0
  44. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  45. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  46. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  47. package/docs/architecture/history.md +3 -34
  48. package/docs/architecture/index.md +15 -98
  49. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  50. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  51. package/docs/architecture/versions/1.6.md +38 -0
  52. package/docs/architecture/versions/1.7.md +41 -0
  53. package/docs/forge-chatgpt-browser-engine.md +2 -2
  54. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  55. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  56. package/docs/forge-github-issue-launcher.md +1 -1
  57. package/docs/forge-runtime-self-healing-loop.md +1 -1
  58. package/docs/operations/controller-browser-plugin.md +26 -6
  59. package/docs/operations/controller-performance-and-502.md +124 -0
  60. package/docs/operations/controller-reliability-runbook.md +1 -1
  61. package/docs/operations/features.md +2 -2
  62. package/docs/operations/github-repository.md +1 -1
  63. package/docs/operations/homebrew.md +2 -2
  64. package/docs/operations/mcp-tool-exposure.md +5 -3
  65. package/docs/wiki/Architecture.md +6 -6
  66. package/docs/wiki/Implementation.md +2 -2
  67. package/docs/wiki/Integrations.md +2 -2
  68. package/docs/wiki/Work-Lifecycle.md +1 -1
  69. package/package.json +29 -10
  70. package/scripts/activate-source-baseline.ts +17 -13
  71. package/scripts/benchmark-background-check-overlap.ts +228 -0
  72. package/scripts/benchmark-mcp-host.sh +117 -0
  73. package/scripts/benchmark-mcp-transport.ts +170 -0
  74. package/scripts/benchmark-quality-harness.ts +146 -0
  75. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  76. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  77. package/scripts/benchmark-typescript-navigation.ts +186 -0
  78. package/scripts/benchmark-work-efficiency.ts +334 -0
  79. package/scripts/capture-plan.sh +3 -3
  80. package/scripts/check-agent-tooling.sh +8 -35
  81. package/scripts/check-architecture-sync.sh +17 -86
  82. package/scripts/check-mcp-compatibility.ts +14 -0
  83. package/scripts/check-runtime-architecture.mjs +107 -64
  84. package/scripts/check-stable-baseline.ts +92 -0
  85. package/scripts/check-task-workflow.sh +4 -4
  86. package/scripts/contract-worktree.sh +1 -21
  87. package/scripts/ensure-task-workflow.sh +22 -55
  88. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  89. package/scripts/install-standalone-recovery.ts +24 -0
  90. package/scripts/lib/project-init-lib.sh +15 -44
  91. package/scripts/migrate-project-template.sh +23 -1179
  92. package/scripts/new-plan.sh +1 -1
  93. package/scripts/plan-to-todo.sh +11 -36
  94. package/scripts/run-governed-gate.ts +2 -1
  95. package/scripts/ship-worktrees.sh +2 -29
  96. package/scripts/smoke-ios-physical-device.ts +4 -2
  97. package/scripts/smoke-runtime-recovery.ts +1 -35
  98. package/scripts/smoke-schedule-engine.ts +1 -20
  99. package/scripts/verify-contract.sh +1 -111
  100. package/scripts/verify-forge-runtime.sh +0 -1
  101. package/scripts/verify-sprint.sh +3 -180
  102. package/scripts/workflow-contract.ts +1 -5
  103. package/src/cli/commands/init-hook.ts +3 -3
  104. package/src/cli/commands/mcp.ts +9 -5
  105. package/src/cli/commands/plugin.ts +1 -1
  106. package/src/cli/commands/recovery.ts +25 -3
  107. package/src/cli/commands/runtime.ts +55 -3
  108. package/src/cli/commands/security.ts +12 -1
  109. package/src/cli/commands/setup-profile.ts +31 -21
  110. package/src/cli/controller/check-runner.ts +106 -3
  111. package/src/cli/controller/composite-result.ts +1 -1
  112. package/src/cli/controller/context/focus.ts +51 -0
  113. package/src/cli/controller/context/instruction-context.ts +114 -0
  114. package/src/cli/controller/context/known-paths.ts +76 -0
  115. package/src/cli/controller/context/query-planning.ts +110 -0
  116. package/src/cli/controller/context/rule-resolution.ts +532 -0
  117. package/src/cli/controller/context/source-materializer.ts +278 -0
  118. package/src/cli/controller/context/structural.ts +16 -0
  119. package/src/cli/controller/context/types.ts +286 -0
  120. package/src/cli/controller/context-pack.ts +785 -469
  121. package/src/cli/controller/launch-agents.ts +77 -2
  122. package/src/cli/controller/operational-plan.ts +2 -2
  123. package/src/cli/controller/work-mode.ts +6 -4
  124. package/src/cli/editing/edit-session.ts +60 -7
  125. package/src/cli/github/github.ts +133 -0
  126. package/src/cli/installer/managed-entries.ts +15 -1
  127. package/src/cli/local-bridge/console-automations.ts +54 -78
  128. package/src/cli/local-bridge/console-view-models.ts +1 -35
  129. package/src/cli/local-bridge/facade-api.ts +0 -217
  130. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  131. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  132. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  133. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  134. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  135. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  136. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  137. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  138. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  139. package/src/cli/local-bridge/job-store.ts +1 -490
  140. package/src/cli/local-bridge/server.ts +0 -513
  141. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  142. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  143. package/src/cli/mcp/auth.ts +5 -5
  144. package/src/cli/mcp/instructions.ts +3 -2
  145. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  146. package/src/cli/mcp/multi-repository.ts +16 -5
  147. package/src/cli/mcp/oauth.ts +175 -26
  148. package/src/cli/mcp/repository-tools.ts +247 -107
  149. package/src/cli/mcp/server.ts +537 -111
  150. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  151. package/src/cli/mcp/toolset-selection.ts +1 -1
  152. package/src/cli/mcp/toolset.ts +42 -5
  153. package/src/cli/mcp/transports/http.ts +101 -14
  154. package/src/cli/mcp/transports/session-registry.ts +43 -5
  155. package/src/cli/mcp/types.ts +3 -3
  156. package/src/cli/repositories/command-classifier.ts +101 -19
  157. package/src/cli/repositories/command-executor.ts +55 -521
  158. package/src/cli/repositories/command-process.ts +108 -0
  159. package/src/cli/repositories/command-scope.ts +99 -41
  160. package/src/cli/repositories/controller-home.ts +7 -8
  161. package/src/cli/repositories/git-command-executor.ts +2 -0
  162. package/src/cli/repositories/locks.ts +53 -16
  163. package/src/cli/repositories/repository-snapshot.ts +121 -0
  164. package/src/cli/repositories/selected-path-actions.ts +17 -9
  165. package/src/cli/repositories/structured-git.ts +61 -3
  166. package/src/cli/repository/inspector.ts +553 -63
  167. package/src/cli/repository/session-cache.ts +58 -17
  168. package/src/cli/v81-entry.ts +1 -4
  169. package/src/effects/fs-transaction.ts +85 -17
  170. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  171. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  172. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  173. package/src/runtime/context/lsp-client.ts +283 -0
  174. package/src/runtime/context/semantic-navigation.ts +275 -0
  175. package/src/runtime/context/swift-navigation.ts +565 -0
  176. package/src/runtime/context/typescript-navigation.ts +226 -0
  177. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  178. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +47 -8
  179. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  180. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  181. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  182. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  183. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  184. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  185. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  186. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
  187. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  188. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
  189. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  190. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  191. package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
  192. package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
  193. package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
  194. package/src/runtime/control-plane/facade/index.ts +1 -0
  195. package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
  196. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  197. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  198. package/src/runtime/control-plane/facade/types.ts +22 -5
  199. package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
  200. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  201. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  202. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  203. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  204. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  205. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  206. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  207. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  208. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  209. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  210. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  211. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  212. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  213. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  214. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  215. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  216. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  217. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  218. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  219. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
  220. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  221. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  222. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  223. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  224. package/src/runtime/control-plane/release-retention.ts +62 -2
  225. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  226. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  227. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  228. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  229. package/src/runtime/diagnostics/performance.ts +24 -1
  230. package/src/runtime/evidence/event-ledger.ts +1 -1
  231. package/src/runtime/execution/jobs/store.ts +58 -4
  232. package/src/runtime/execution/jobs/types.ts +1 -1
  233. package/src/runtime/execution/managed-workspace.ts +203 -1
  234. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  235. package/src/runtime/execution/process-runtime/check-result.ts +53 -0
  236. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  237. package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
  238. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  239. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  240. package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
  241. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  242. package/src/runtime/execution/process-runtime/runtime.ts +43 -3
  243. package/src/runtime/execution/process-runtime/store.ts +1 -0
  244. package/src/runtime/execution/process-runtime/types.ts +2 -0
  245. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  246. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  247. package/src/runtime/execution/workers/executor.ts +0 -48
  248. package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
  249. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  250. package/src/runtime/gateway/mcp/router.ts +184 -82
  251. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  252. package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
  253. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  254. package/src/runtime/plugins/browser-adapter.ts +2901 -241
  255. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  256. package/src/runtime/plugins/browser-handoff.ts +92 -2
  257. package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
  258. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  259. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  260. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  261. package/src/runtime/plugins/browser-runtime.ts +183 -0
  262. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  263. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  264. package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
  265. package/src/runtime/plugins/external-adapter.ts +324 -3
  266. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  267. package/src/runtime/plugins/first-party-registry.ts +2 -0
  268. package/src/runtime/plugins/github-adapter.ts +125 -0
  269. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  270. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  271. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  272. package/src/runtime/plugins/ios-adapter.ts +106 -16
  273. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  274. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  275. package/src/runtime/plugins/lightweight-action.ts +153 -0
  276. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  277. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  278. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  279. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  280. package/src/runtime/plugins/resend-adapter.ts +1 -1
  281. package/src/runtime/plugins/store.ts +269 -29
  282. package/src/runtime/plugins/types.ts +35 -2
  283. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  284. package/src/runtime/projections/git-status-sampler.ts +16 -16
  285. package/src/runtime/projections/materialized-view.ts +28 -10
  286. package/src/runtime/recovery/maintenance-executor.ts +393 -28
  287. package/src/runtime/root/mcp-transport.ts +180 -10
  288. package/src/runtime/root/package-connector-service.ts +186 -8
  289. package/src/runtime/root/package-runtime-release.ts +143 -17
  290. package/src/runtime/root/package-runtime-service.ts +430 -22
  291. package/src/runtime/root/release-manifest.ts +7 -2
  292. package/src/runtime/root/release-materialize.ts +19 -0
  293. package/src/runtime/root/release-store.ts +48 -11
  294. package/src/runtime/root/runtime.ts +23 -5
  295. package/src/runtime/root/service.ts +100 -29
  296. package/src/runtime/root/types.ts +2 -0
  297. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  298. package/src/runtime/shared/trusted-executable.ts +62 -0
  299. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  300. package/src/runtime/standalone-recovery/core.ts +700 -92
  301. package/src/runtime/standalone-recovery/entry.ts +99 -23
  302. package/src/runtime/standalone-recovery/installer.ts +19 -3
  303. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  304. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  305. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  306. package/src/runtime/workflow/schedules/engine.ts +287 -151
  307. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  308. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  309. package/src/runtime/workflow/schedules/store.ts +131 -10
  310. package/src/runtime/workflow/schedules/types.ts +2 -4
  311. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  312. package/src/testing/test-governance.ts +8 -40
  313. package/assets/reference-configs/heartbeat-triage.md +0 -57
  314. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  315. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  316. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  317. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  318. package/docs/architecture/current/architecture-invariants.md +0 -331
  319. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  320. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  321. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  322. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  323. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  324. package/docs/architecture/current/entity-model.md +0 -612
  325. package/docs/architecture/current/external-plugin-governance.md +0 -396
  326. package/docs/architecture/current/failure-recovery.md +0 -447
  327. package/docs/architecture/current/governance.md +0 -213
  328. package/docs/architecture/current/human-interaction-plane.md +0 -87
  329. package/docs/architecture/current/implementation-status.md +0 -128
  330. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  331. package/docs/architecture/current/legacy-governance.md +0 -58
  332. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  333. package/docs/architecture/current/migration-roadmap.md +0 -105
  334. package/docs/architecture/current/multi-repository-execution.md +0 -371
  335. package/docs/architecture/current/operations-runbook.md +0 -45
  336. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  337. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  338. package/docs/architecture/current/repository-access-modes.md +0 -102
  339. package/docs/architecture/current/route-policy.md +0 -15
  340. package/docs/architecture/current/route-session-concurrency.md +0 -85
  341. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  342. package/docs/architecture/current/runtime-directory-map.md +0 -49
  343. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  344. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  345. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  346. package/docs/architecture/current/system-overview.md +0 -169
  347. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  348. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  349. package/docs/architecture/current/thin-harness-v1.md +0 -321
  350. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  351. package/docs/forge-autonomous-goal-loop.md +0 -9
  352. package/docs/forge-provider-configuration.md +0 -7
  353. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  354. package/docs/operations/gmail-assistant-routines.md +0 -186
  355. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  356. package/docs/operations/provider-configuration.md +0 -31
  357. package/scripts/harness-trace-grade.sh +0 -150
  358. package/scripts/heartbeat-triage.sh +0 -361
  359. package/scripts/maintenance-triage.sh +0 -167
  360. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  361. package/src/cli/repositories/goal-registry.ts +0 -239
  362. package/src/runtime/assistant/action-proposals.ts +0 -279
  363. package/src/runtime/assistant/intent.ts +0 -395
  364. package/src/runtime/assistant/model-provider.ts +0 -460
  365. package/src/runtime/assistant/openapi.ts +0 -223
  366. package/src/runtime/assistant/policy.ts +0 -89
  367. package/src/runtime/assistant/readiness.ts +0 -258
  368. package/src/runtime/assistant/routine-runtime.ts +0 -477
  369. package/src/runtime/assistant/schedule-binding.ts +0 -171
  370. package/src/runtime/assistant/standing-grants.ts +0 -375
  371. package/src/runtime/assistant/store.ts +0 -243
  372. package/src/runtime/assistant/types.ts +0 -135
  373. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  374. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  375. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  376. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  377. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  378. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  379. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  380. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  381. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  382. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  383. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  384. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  385. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  386. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  387. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  388. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  389. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  390. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  391. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  392. package/src/runtime/workflow/findings/store.ts +0 -204
  393. package/src/runtime/workflow/findings/types.ts +0 -58
  394. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  395. package/src/runtime/workflow/portfolio/store.ts +0 -185
  396. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -1,12 +1,15 @@
1
1
  import { createHash } from 'crypto';
2
+ import { execFile } from 'child_process';
2
3
  import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs';
3
- import { join } from 'path';
4
+ import { lstat as lstatAsync, readFile as readFileAsync, readdir as readdirAsync, stat as statAsync } from 'fs/promises';
5
+ import { join, resolve } from 'path';
4
6
  import { runProcess } from '../../effects/process-runner';
5
7
  import { globMatches, resolveMcpPath } from '../mcp/paths';
6
8
  import type { McpPolicy } from '../mcp/types';
7
9
  import {
8
10
  collectSessionIdentity,
9
11
  getOrCreateSessionCache,
12
+ invalidateSessionCachesForRepository,
10
13
  type RepositorySessionCache,
11
14
  type SessionIdentity,
12
15
  } from './session-cache';
@@ -36,6 +39,8 @@ export interface CachedGitIdentity {
36
39
  sampledAt: number;
37
40
  head: string | null;
38
41
  branch: string | null;
42
+ /** Dirty state comes from the same porcelain-v2 sample as branch/HEAD. */
43
+ dirty: boolean;
39
44
  workingTreeFingerprint?: string;
40
45
  }
41
46
 
@@ -76,7 +81,7 @@ function sampleGitIdentity(repoRoot: string): Omit<CachedGitIdentity, 'sampledAt
76
81
  timeoutMs: 10_000,
77
82
  maxOutputBytes: 4 * 1024 * 1024,
78
83
  });
79
- if (!result.ok) return { head: null, branch: null };
84
+ if (!result.ok) return { head: null, branch: null, dirty: false };
80
85
 
81
86
  let head: string | null = null;
82
87
  let branch: string | null = null;
@@ -93,13 +98,43 @@ function sampleGitIdentity(repoRoot: string): Omit<CachedGitIdentity, 'sampledAt
93
98
  }
94
99
  }
95
100
  const status = statusLines.join('\n');
101
+ let dirtyBytes = 0;
102
+ const dirtyContentIdentity = statusLines.slice(0, 500).map((line) => {
103
+ const path = line.startsWith('? ') || line.startsWith('! ')
104
+ ? line.slice(2)
105
+ : line.startsWith('1 ')
106
+ ? line.split(' ').slice(8).join(' ')
107
+ : line.startsWith('2 ')
108
+ ? line.split(' ').slice(9).join(' ').split('\t')[0]
109
+ : line.startsWith('u ')
110
+ ? line.split(' ').slice(10).join(' ')
111
+ : '';
112
+ if (!path) return line;
113
+ try {
114
+ const absolute = join(repoRoot, path);
115
+ const stat = lstatSync(absolute);
116
+ if (!stat.isFile()) return `${path}:${stat.mode}:${stat.size}:${stat.mtimeMs}`;
117
+ if (stat.size <= 256 * 1024 && dirtyBytes + stat.size <= 4 * 1024 * 1024) {
118
+ dirtyBytes += stat.size;
119
+ return `${path}:${createHash('sha256').update(readFileSync(absolute)).digest('hex')}`;
120
+ }
121
+ return `${path}:${stat.size}:${stat.mtimeMs}`;
122
+ } catch {
123
+ return `${path}:missing`;
124
+ }
125
+ }).join('\n');
96
126
  const workingTreeFingerprint = createHash('sha256')
97
- .update(`${head ?? ''}\n${branch ?? ''}\n${status}`)
127
+ .update(`${head ?? ''}\n${branch ?? ''}\n${status}\n${dirtyContentIdentity}`)
98
128
  .digest('hex')
99
129
  .slice(0, 24);
100
- return { head, branch, workingTreeFingerprint: head || status ? workingTreeFingerprint : undefined };
130
+ return {
131
+ head,
132
+ branch,
133
+ dirty: statusLines.length > 0,
134
+ workingTreeFingerprint: head || status ? workingTreeFingerprint : undefined,
135
+ };
101
136
  } catch {
102
- return { head: null, branch: null };
137
+ return { head: null, branch: null, dirty: false };
103
138
  }
104
139
  }
105
140
 
@@ -110,18 +145,46 @@ function sampleGitIdentity(repoRoot: string): Omit<CachedGitIdentity, 'sampledAt
110
145
  * fingerprint is content-based (stable across reads) and the short TTL window
111
146
  * keeps checkout routing safe while real mutations invalidate through markers.
112
147
  */
148
+ export function freshGitIdentity(repoRoot: string): CachedGitIdentity {
149
+ const root = resolve(repoRoot);
150
+ const sampled = sampleGitIdentity(root);
151
+ const entry: CachedGitIdentity = { ...sampled, sampledAt: Date.now() };
152
+ gitIdentityCache.set(root, entry);
153
+ pruneGitIdentityCache();
154
+ return entry;
155
+ }
156
+
113
157
  export function cachedGitIdentity(repoRoot: string): CachedGitIdentity {
158
+ const root = resolve(repoRoot);
114
159
  const now = Date.now();
115
- const existing = gitIdentityCache.get(repoRoot);
160
+ const existing = gitIdentityCache.get(root);
116
161
  if (existing && now - existing.sampledAt < GIT_IDENTITY_SAMPLE_TTL_MS) {
117
162
  gitIdentityPerformance.cacheHits += 1;
118
163
  return existing;
119
164
  }
120
- const sampled = sampleGitIdentity(repoRoot);
121
- const entry: CachedGitIdentity = { ...sampled, sampledAt: now };
122
- gitIdentityCache.set(repoRoot, entry);
123
- pruneGitIdentityCache();
124
- return entry;
165
+ return freshGitIdentity(root);
166
+ }
167
+
168
+ /**
169
+ * Successful Forge-owned mutations invalidate observations derived from the
170
+ * previous source state. This is intentionally lazy: no graph rebuild, check,
171
+ * or language server is started here; the next read re-samples Git/current bytes.
172
+ */
173
+ export function invalidateRepositoryReadCaches(repoRoot: string): void {
174
+ const root = resolve(repoRoot);
175
+ gitIdentityCache.delete(root);
176
+ gitSnapshotCache.delete(root);
177
+ for (const key of [...searchFileInventoryCache.keys()]) {
178
+ try {
179
+ const identity = JSON.parse(key) as { repoRoot?: unknown };
180
+ if (typeof identity.repoRoot === 'string' && resolve(identity.repoRoot) === root) {
181
+ searchFileInventoryCache.delete(key);
182
+ }
183
+ } catch {
184
+ // Cache keys are internal JSON identities. Ignore legacy/unparseable entries.
185
+ }
186
+ }
187
+ invalidateSessionCachesForRepository(root);
125
188
  }
126
189
 
127
190
  export function clearGitIdentityCacheForTest(): void {
@@ -159,14 +222,26 @@ export interface RepositoryReadSession {
159
222
 
160
223
  function bindSessionCache(repoRoot: string, session?: RepositoryReadSession): RepositorySessionCache | null {
161
224
  if (!session?.sessionId || !session.repoId || !session.checkoutId) return null;
162
- const identity = collectSessionIdentity({
163
- repoRoot,
225
+ const sampled = cachedGitIdentity(repoRoot);
226
+ const identity: SessionIdentity = {
164
227
  repoId: session.repoId,
165
228
  checkoutId: session.checkoutId,
166
- });
229
+ branch: sampled.branch,
230
+ head: sampled.head,
231
+ workingTreeFingerprint: sampled.workingTreeFingerprint
232
+ ?? createHash('sha256').update(`${sampled.head ?? ''}\n${sampled.branch ?? ''}`).digest('hex').slice(0, 24),
233
+ };
167
234
  return getOrCreateSessionCache(session.sessionId, repoRoot, identity);
168
235
  }
169
236
 
237
+ /** Shared non-durable cache binding for progressive Context Plane providers. */
238
+ export function getRepositoryReadSessionCache(
239
+ repoRoot: string,
240
+ session?: RepositoryReadSession,
241
+ ): RepositorySessionCache | null {
242
+ return bindSessionCache(repoRoot, session);
243
+ }
244
+
170
245
  export function resolveSessionIdentity(
171
246
  repoRoot: string,
172
247
  repoId: string,
@@ -208,6 +283,15 @@ const SEARCH_GIT_INVENTORY_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
208
283
  * output is truncated so non-Git/ephemeral workspaces retain the filesystem
209
284
  * fallback below.
210
285
  */
286
+ function parseGitSearchFileInventory(stdout: string): string[] {
287
+ return stdout
288
+ .split('\0')
289
+ // NUL framing already gives exact Git paths; do not trim because leading
290
+ // or trailing whitespace is legal in a repository filename.
291
+ .filter((path) => path.length > 0)
292
+ .sort((left, right) => left.localeCompare(right));
293
+ }
294
+
211
295
  function gitSearchFileInventory(repoRoot: string): string[] | null {
212
296
  try {
213
297
  const result = runProcess('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
@@ -216,17 +300,34 @@ function gitSearchFileInventory(repoRoot: string): string[] | null {
216
300
  maxOutputBytes: SEARCH_GIT_INVENTORY_MAX_OUTPUT_BYTES,
217
301
  });
218
302
  if (!result.ok || result.stdout.includes('[output truncated after')) return null;
219
- return result.stdout
220
- .split('\0')
221
- // NUL framing already gives exact Git paths; do not trim because leading
222
- // or trailing whitespace is legal in a repository filename.
223
- .filter((path) => path.length > 0)
224
- .sort((left, right) => left.localeCompare(right));
303
+ return parseGitSearchFileInventory(result.stdout);
225
304
  } catch {
226
305
  return null;
227
306
  }
228
307
  }
229
308
 
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
+
230
331
  function binary(bytes: Buffer): boolean {
231
332
  return bytes.subarray(0, Math.min(bytes.length, 8000)).includes(0);
232
333
  }
@@ -251,6 +352,36 @@ function walk(repoRoot: string, root: string, maxFiles: number, excludes: string
251
352
  }
252
353
  }
253
354
 
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
+
254
385
  export interface SearchRepositoryOptions {
255
386
  query: string;
256
387
  includeGlobs?: string[];
@@ -272,6 +403,18 @@ export interface SearchRepositoryManyOptions {
272
403
  maxFiles?: number;
273
404
  caseSensitive?: boolean;
274
405
  cacheKey?: string;
406
+ /**
407
+ * Strict search remains the default. Broad Context discovery may stop once it
408
+ * has enough distinct candidate files across several query hints; guessed
409
+ * natural-language terms are evidence hints, not completeness obligations.
410
+ */
411
+ completionMode?: 'per_query' | 'discovery';
412
+ discoveryTargetFiles?: number;
413
+ discoveryMinQueryCoverage?: number;
414
+ /** Exact caller-supplied needles that discovery must not skip. */
415
+ requiredQueries?: string[];
416
+ /** MCP/controller session identity used for progressive follow-up reuse. */
417
+ session?: RepositoryReadSession;
275
418
  }
276
419
 
277
420
  export interface SearchRepositoryManyResult {
@@ -282,10 +425,76 @@ export interface SearchRepositoryManyResult {
282
425
  skippedLargeFiles: number;
283
426
  skippedBinaryFiles: number;
284
427
  truncated: boolean;
285
- truncationReason?: 'max_results' | 'max_files';
428
+ truncationReason?: 'max_results' | 'max_files' | 'discovery_budget';
286
429
  cacheHit?: boolean;
287
430
  }
288
431
 
432
+ export interface SearchRepositoryManyCacheIdentity {
433
+ queries: string[];
434
+ maxResultsPerQuery: number;
435
+ maxFiles: number;
436
+ includes: string[];
437
+ excludes: string[];
438
+ directCandidates?: string[];
439
+ completionMode: 'per_query' | 'discovery';
440
+ discoveryTargetFiles: number;
441
+ discoveryMinQueryCoverage: number;
442
+ requiredQueries: string[];
443
+ includeKey: string;
444
+ batchQueryKey: string;
445
+ }
446
+
447
+ /** Shared normalization/cache identity for sync search and worker-prefetched search. */
448
+ export function searchRepositoryManyCacheIdentity(opts: SearchRepositoryManyOptions): SearchRepositoryManyCacheIdentity {
449
+ const queries = Array.from(new Set(opts.queries.map((query) => query.trim()).filter(Boolean)));
450
+ if (queries.length === 0) throw new Error('at least one search query is required');
451
+ const maxResultsPerQuery = Math.min(Math.max(opts.maxResultsPerQuery ?? 100, 1), 500);
452
+ const maxFiles = Math.min(Math.max(opts.maxFiles ?? 5000, 1), 20_000);
453
+ const includes = opts.includeGlobs ?? [];
454
+ const excludes = [...(includes.length === 0 ? DEFAULT_EXCLUDES : []), ...(opts.excludeGlobs ?? [])];
455
+ const directFiles = opts.files?.map((path) => path.trim()).filter(Boolean);
456
+ const directCandidates = directFiles
457
+ ? Array.from(new Set(directFiles)).filter((path) => !isExcluded(path, excludes))
458
+ : undefined;
459
+ const completionMode = opts.completionMode === 'discovery' ? 'discovery' : 'per_query';
460
+ const discoveryTargetFiles = completionMode === 'discovery'
461
+ ? Math.min(Math.max(opts.discoveryTargetFiles ?? Math.max(12, Math.ceil(maxResultsPerQuery / 2)), 1), maxFiles)
462
+ : maxFiles;
463
+ const discoveryMinQueryCoverage = completionMode === 'discovery'
464
+ ? Math.min(Math.max(opts.discoveryMinQueryCoverage ?? Math.min(3, queries.length), 1), queries.length)
465
+ : queries.length;
466
+ const requiredQueries = completionMode === 'discovery'
467
+ ? Array.from(new Set((opts.requiredQueries ?? []).map((query) => query.trim()).filter((query) => queries.includes(query))))
468
+ : [];
469
+ const includeKey = JSON.stringify({
470
+ files: directCandidates,
471
+ includes,
472
+ excludes,
473
+ maxResultsPerQuery,
474
+ maxFiles,
475
+ caseSensitive: opts.caseSensitive === true,
476
+ cacheKey: opts.cacheKey,
477
+ completionMode,
478
+ discoveryTargetFiles,
479
+ discoveryMinQueryCoverage,
480
+ requiredQueries,
481
+ });
482
+ return {
483
+ queries,
484
+ maxResultsPerQuery,
485
+ maxFiles,
486
+ includes,
487
+ excludes,
488
+ directCandidates,
489
+ completionMode,
490
+ discoveryTargetFiles,
491
+ discoveryMinQueryCoverage,
492
+ requiredQueries,
493
+ includeKey,
494
+ batchQueryKey: queries.join('\u0000'),
495
+ };
496
+ }
497
+
289
498
  function searchInventory(
290
499
  repoRoot: string,
291
500
  includes: string[],
@@ -313,6 +522,157 @@ function searchInventory(
313
522
  return { files: candidates.slice(0, maxFiles), candidateCount: candidates.length, cacheHit };
314
523
  }
315
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 };
552
+ }
553
+
554
+ interface SearchManyMatchState {
555
+ queries: string[];
556
+ needles: Array<{ query: string; needle: string }>;
557
+ counts: Map<string, number>;
558
+ results: SearchRepositoryManyResult['results'];
559
+ matchedPaths: Set<string>;
560
+ maxResultsPerQuery: number;
561
+ completionMode: 'per_query' | 'discovery';
562
+ discoveryTargetFiles: number;
563
+ discoveryMinQueryCoverage: number;
564
+ requiredQueries: string[];
565
+ caseSensitive: boolean;
566
+ }
567
+
568
+ function createSearchManyMatchState(
569
+ queries: string[],
570
+ maxResultsPerQuery: number,
571
+ completionMode: 'per_query' | 'discovery',
572
+ discoveryTargetFiles: number,
573
+ discoveryMinQueryCoverage: number,
574
+ requiredQueries: string[],
575
+ caseSensitive: boolean,
576
+ ): SearchManyMatchState {
577
+ return {
578
+ queries,
579
+ needles: queries.map((query) => ({ query, needle: caseSensitive ? query : query.toLowerCase() })),
580
+ counts: new Map(queries.map((query) => [query, 0])),
581
+ results: [],
582
+ matchedPaths: new Set<string>(),
583
+ maxResultsPerQuery,
584
+ completionMode,
585
+ discoveryTargetFiles,
586
+ discoveryMinQueryCoverage,
587
+ requiredQueries,
588
+ caseSensitive,
589
+ };
590
+ }
591
+
592
+ function searchManyComplete(state: SearchManyMatchState): boolean {
593
+ if (state.completionMode === 'per_query') {
594
+ return state.needles.every(({ query }) => (state.counts.get(query) ?? 0) >= state.maxResultsPerQuery);
595
+ }
596
+ if (state.requiredQueries.some((query) => (state.counts.get(query) ?? 0) === 0)) return false;
597
+ const coveredQueries = state.needles.reduce(
598
+ (count, { query }) => count + ((state.counts.get(query) ?? 0) > 0 ? 1 : 0),
599
+ 0,
600
+ );
601
+ return state.matchedPaths.size >= state.discoveryTargetFiles
602
+ && coveredQueries >= state.discoveryMinQueryCoverage;
603
+ }
604
+
605
+ function exactCodeNeedleMatches(haystack: string, needle: string): boolean {
606
+ const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
607
+ return new RegExp(`(?:^|[^\\p{L}\\p{N}_$])${escaped}(?=$|[^\\p{L}\\p{N}_$])`, 'u').test(haystack);
608
+ }
609
+
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
+ }
633
+ }
634
+ }
635
+
636
+ function finalizeSearchManyResult(input: {
637
+ state: SearchManyMatchState;
638
+ scannedFiles: number;
639
+ policyDeniedFiles: number;
640
+ skippedLargeFiles: number;
641
+ skippedBinaryFiles: number;
642
+ candidateCount: number;
643
+ inventoryFileCount: number;
644
+ inventoryCacheHit: boolean;
645
+ }): SearchRepositoryManyResult {
646
+ const { state } = input;
647
+ const queryPriority = new Map(state.queries.map((query, index) => [query, index]));
648
+ state.results.sort((left, right) => {
649
+ const priority = (queryPriority.get(left.query) ?? state.queries.length) - (queryPriority.get(right.query) ?? state.queries.length);
650
+ if (priority !== 0) return priority;
651
+ const pathOrder = left.path.localeCompare(right.path);
652
+ return pathOrder !== 0 ? pathOrder : left.line - right.line;
653
+ });
654
+ const discoveryBudgetReached = state.completionMode === 'discovery' && searchManyComplete(state);
655
+ const hitResultLimit = state.needles.some(({ query }) => (state.counts.get(query) ?? 0) >= state.maxResultsPerQuery);
656
+ const truncationReason = discoveryBudgetReached
657
+ ? 'discovery_budget' as const
658
+ : hitResultLimit
659
+ ? 'max_results' as const
660
+ : input.candidateCount > input.inventoryFileCount
661
+ ? 'max_files' as const
662
+ : undefined;
663
+ return {
664
+ queries: state.queries,
665
+ results: state.results,
666
+ scannedFiles: input.scannedFiles,
667
+ policyDeniedFiles: input.policyDeniedFiles,
668
+ skippedLargeFiles: input.skippedLargeFiles,
669
+ skippedBinaryFiles: input.skippedBinaryFiles,
670
+ truncated: Boolean(truncationReason),
671
+ truncationReason,
672
+ cacheHit: input.inventoryCacheHit,
673
+ };
674
+ }
675
+
316
676
  /**
317
677
  * Internal Context Plane batch search. Every candidate file is policy-checked
318
678
  * and read at most once while all lexical terms are matched in the same pass.
@@ -323,16 +683,27 @@ export function searchRepositoryMany(
323
683
  policy: McpPolicy,
324
684
  opts: SearchRepositoryManyOptions,
325
685
  ): SearchRepositoryManyResult {
326
- const queries = Array.from(new Set(opts.queries.map((query) => query.trim()).filter(Boolean)));
327
- if (queries.length === 0) throw new Error('at least one search query is required');
328
- const maxResultsPerQuery = Math.min(Math.max(opts.maxResultsPerQuery ?? 100, 1), 500);
329
- const maxFiles = Math.min(Math.max(opts.maxFiles ?? 5000, 1), 20_000);
330
- const includes = opts.includeGlobs ?? [];
331
- const excludes = [...(includes.length === 0 ? DEFAULT_EXCLUDES : []), ...(opts.excludeGlobs ?? [])];
332
- const directFiles = opts.files?.map((path) => path.trim()).filter(Boolean);
333
- const directCandidates = directFiles
334
- ? Array.from(new Set(directFiles)).filter((path) => !isExcluded(path, excludes))
335
- : undefined;
686
+ const {
687
+ queries,
688
+ maxResultsPerQuery,
689
+ maxFiles,
690
+ includes,
691
+ excludes,
692
+ directCandidates,
693
+ completionMode,
694
+ discoveryTargetFiles,
695
+ discoveryMinQueryCoverage,
696
+ requiredQueries,
697
+ includeKey,
698
+ batchQueryKey,
699
+ } = searchRepositoryManyCacheIdentity(opts);
700
+ const sessionCache = bindSessionCache(repoRoot, opts.session);
701
+ if (sessionCache) {
702
+ const cached = sessionCache.getSearch(batchQueryKey, includeKey);
703
+ if (cached?.result && typeof cached.result === 'object') {
704
+ return { ...(cached.result as SearchRepositoryManyResult), cacheHit: true };
705
+ }
706
+ }
336
707
  const inventory = directCandidates && directCandidates.length > 0
337
708
  ? {
338
709
  files: directCandidates.slice(0, maxFiles),
@@ -340,9 +711,15 @@ export function searchRepositoryMany(
340
711
  cacheHit: false,
341
712
  }
342
713
  : searchInventory(repoRoot, includes, excludes, maxFiles, opts.cacheKey);
343
- const needles = queries.map((query) => ({ query, needle: opts.caseSensitive ? query : query.toLowerCase() }));
344
- const counts = new Map(queries.map((query) => [query, 0]));
345
- const results: SearchRepositoryManyResult['results'] = [];
714
+ const matchState = createSearchManyMatchState(
715
+ queries,
716
+ maxResultsPerQuery,
717
+ completionMode,
718
+ discoveryTargetFiles,
719
+ discoveryMinQueryCoverage,
720
+ requiredQueries,
721
+ opts.caseSensitive === true,
722
+ );
346
723
  let scannedFiles = 0;
347
724
  let policyDeniedFiles = 0;
348
725
  let skippedLargeFiles = 0;
@@ -350,7 +727,7 @@ export function searchRepositoryMany(
350
727
  const canonicalRepoRoot = realpathSync(repoRoot);
351
728
 
352
729
  for (const path of inventory.files) {
353
- if (needles.every(({ query }) => (counts.get(query) ?? 0) >= maxResultsPerQuery)) break;
730
+ if (searchManyComplete(matchState)) break;
354
731
  const decision = resolveMcpPath(repoRoot, path, policy, 'read', canonicalRepoRoot);
355
732
  if (!decision.ok || !decision.absolutePath) {
356
733
  policyDeniedFiles += 1;
@@ -367,46 +744,159 @@ export function searchRepositoryMany(
367
744
  continue;
368
745
  }
369
746
  scannedFiles += 1;
370
- const lines = bytes.toString('utf-8').split(/\r?\n/);
371
- for (let index = 0; index < lines.length; index += 1) {
372
- const text = lines[index]!;
373
- const haystack = opts.caseSensitive ? text : text.toLowerCase();
374
- for (const { query, needle } of needles) {
375
- if ((counts.get(query) ?? 0) >= maxResultsPerQuery || !haystack.includes(needle)) continue;
376
- results.push({ query, path, line: index + 1, text: text.slice(0, 500) });
377
- counts.set(query, (counts.get(query) ?? 0) + 1);
378
- }
379
- }
747
+ matchSearchManySource(matchState, path, bytes);
380
748
  }
381
749
 
382
750
  // Consumers may bound distinct candidate files after this call. Preserve the
383
751
  // caller's query priority so an exact symbol/phrase cannot be displaced by
384
752
  // earlier alphabetic files that only matched broad recall tokens.
385
- const queryPriority = new Map(queries.map((query, index) => [query, index]));
386
- results.sort((left, right) => {
387
- const priority = (queryPriority.get(left.query) ?? queries.length) - (queryPriority.get(right.query) ?? queries.length);
388
- if (priority !== 0) return priority;
389
- const pathOrder = left.path.localeCompare(right.path);
390
- return pathOrder !== 0 ? pathOrder : left.line - right.line;
753
+ const payload = finalizeSearchManyResult({
754
+ state: matchState,
755
+ scannedFiles,
756
+ policyDeniedFiles,
757
+ skippedLargeFiles,
758
+ skippedBinaryFiles,
759
+ candidateCount: inventory.candidateCount,
760
+ inventoryFileCount: inventory.files.length,
761
+ inventoryCacheHit: inventory.cacheHit,
391
762
  });
763
+ if (sessionCache) {
764
+ sessionCache.putSearch({
765
+ query: batchQueryKey,
766
+ includeKey,
767
+ result: payload,
768
+ scannedFiles,
769
+ });
770
+ }
771
+ return payload;
772
+ }
392
773
 
393
- const hitResultLimit = needles.some(({ query }) => (counts.get(query) ?? 0) >= maxResultsPerQuery);
394
- const truncationReason = hitResultLimit
395
- ? 'max_results' as const
396
- : inventory.candidateCount > inventory.files.length
397
- ? 'max_files' as const
398
- : undefined;
399
- return {
774
+ // File reads are already bounded by policy/maxFiles. A wider batch keeps broad
775
+ // Context scans from serializing dozens of filesystem waves while still
776
+ // preserving original-order matching and early-stop semantics below.
777
+ const ASYNC_SEARCH_READ_BATCH_SIZE = 64;
778
+
779
+ type AsyncSearchCandidate =
780
+ | { path: string; kind: 'denied' }
781
+ | { path: string; kind: 'large' }
782
+ | { path: string; kind: 'binary' }
783
+ | { path: string; kind: 'source'; bytes: Buffer }
784
+ | { path: string; kind: 'error'; error: unknown };
785
+
786
+ async function readSearchCandidateAsync(
787
+ repoRoot: string,
788
+ policy: McpPolicy,
789
+ canonicalRepoRoot: string,
790
+ path: string,
791
+ ): Promise<AsyncSearchCandidate> {
792
+ try {
793
+ const decision = resolveMcpPath(repoRoot, path, policy, 'read', canonicalRepoRoot);
794
+ if (!decision.ok || !decision.absolutePath) return { path, kind: 'denied' };
795
+ const size = (await statAsync(decision.absolutePath)).size;
796
+ if (size > policy.maxFileBytes) return { path, kind: 'large' };
797
+ const bytes = await readFileAsync(decision.absolutePath);
798
+ if (binary(bytes)) return { path, kind: 'binary' };
799
+ return { path, kind: 'source', bytes };
800
+ } catch (error) {
801
+ return { path, kind: 'error', error };
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Async Context Plane batch search with result semantics matching
807
+ * searchRepositoryMany. Candidate reads happen in bounded batches, while policy
808
+ * accounting, early-stop behavior, matching, and result ordering are consumed in
809
+ * original repository order on the caller thread.
810
+ */
811
+ export async function searchRepositoryManyAsync(
812
+ repoRoot: string,
813
+ policy: McpPolicy,
814
+ opts: SearchRepositoryManyOptions,
815
+ ): Promise<SearchRepositoryManyResult> {
816
+ const {
400
817
  queries,
401
- results,
818
+ maxResultsPerQuery,
819
+ maxFiles,
820
+ includes,
821
+ excludes,
822
+ directCandidates,
823
+ completionMode,
824
+ discoveryTargetFiles,
825
+ discoveryMinQueryCoverage,
826
+ requiredQueries,
827
+ includeKey,
828
+ batchQueryKey,
829
+ } = searchRepositoryManyCacheIdentity(opts);
830
+ const sessionCache = bindSessionCache(repoRoot, opts.session);
831
+ if (sessionCache) {
832
+ const cached = sessionCache.getSearch(batchQueryKey, includeKey);
833
+ if (cached?.result && typeof cached.result === 'object') {
834
+ return { ...(cached.result as SearchRepositoryManyResult), cacheHit: true };
835
+ }
836
+ }
837
+ const inventory = directCandidates && directCandidates.length > 0
838
+ ? {
839
+ files: directCandidates.slice(0, maxFiles),
840
+ candidateCount: directCandidates.length,
841
+ cacheHit: false,
842
+ }
843
+ : await searchInventoryAsync(repoRoot, includes, excludes, maxFiles, opts.cacheKey);
844
+ const matchState = createSearchManyMatchState(
845
+ queries,
846
+ maxResultsPerQuery,
847
+ completionMode,
848
+ discoveryTargetFiles,
849
+ discoveryMinQueryCoverage,
850
+ requiredQueries,
851
+ opts.caseSensitive === true,
852
+ );
853
+ let scannedFiles = 0;
854
+ let policyDeniedFiles = 0;
855
+ let skippedLargeFiles = 0;
856
+ let skippedBinaryFiles = 0;
857
+ const canonicalRepoRoot = realpathSync(repoRoot);
858
+ for (let offset = 0; offset < inventory.files.length && !searchManyComplete(matchState); offset += ASYNC_SEARCH_READ_BATCH_SIZE) {
859
+ const batchPaths = inventory.files.slice(offset, offset + ASYNC_SEARCH_READ_BATCH_SIZE);
860
+ const batch = await Promise.all(batchPaths.map((path) => readSearchCandidateAsync(repoRoot, policy, canonicalRepoRoot, path)));
861
+ for (const candidate of batch) {
862
+ if (searchManyComplete(matchState)) break;
863
+ if (candidate.kind === 'denied') {
864
+ policyDeniedFiles += 1;
865
+ continue;
866
+ }
867
+ if (candidate.kind === 'large') {
868
+ skippedLargeFiles += 1;
869
+ continue;
870
+ }
871
+ if (candidate.kind === 'binary') {
872
+ skippedBinaryFiles += 1;
873
+ continue;
874
+ }
875
+ if (candidate.kind === 'error') throw candidate.error;
876
+ scannedFiles += 1;
877
+ matchSearchManySource(matchState, candidate.path, candidate.bytes);
878
+ }
879
+ }
880
+
881
+ const payload = finalizeSearchManyResult({
882
+ state: matchState,
402
883
  scannedFiles,
403
884
  policyDeniedFiles,
404
885
  skippedLargeFiles,
405
886
  skippedBinaryFiles,
406
- truncated: Boolean(truncationReason),
407
- truncationReason,
408
- cacheHit: inventory.cacheHit,
409
- };
887
+ candidateCount: inventory.candidateCount,
888
+ inventoryFileCount: inventory.files.length,
889
+ inventoryCacheHit: inventory.cacheHit,
890
+ });
891
+ if (sessionCache) {
892
+ sessionCache.putSearch({
893
+ query: batchQueryKey,
894
+ includeKey,
895
+ result: payload,
896
+ scannedFiles,
897
+ });
898
+ }
899
+ return payload;
410
900
  }
411
901
 
412
902
  export function searchRepository(repoRoot: string, policy: McpPolicy, opts: SearchRepositoryOptions & {