@moretea-labs/forge 1.5.1 → 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 (434) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +46 -0
  3. package/CLAUDE.md +2 -0
  4. package/DESIGN.md +7 -2
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +2 -2
  7. package/SKILL.md +2 -2
  8. package/THIRD_PARTY_NOTICES.md +1 -1
  9. package/agents/openai.yaml +1 -1
  10. package/assets/hooks/hook-input.sh +11 -8
  11. package/assets/hooks/lib/session-state.sh +10 -7
  12. package/assets/hooks/lib/workflow-state.sh +13 -177
  13. package/assets/hooks/post-tool-observer.sh +8 -2
  14. package/assets/hooks/pre-edit-guard.sh +8 -39
  15. package/assets/hooks/prompt-guard.sh +7 -83
  16. package/assets/hooks/session-start-context.sh +56 -1
  17. package/assets/partials/05-workflow.partial.md +1 -1
  18. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  19. package/assets/plugin-registry.v1.json +54 -3
  20. package/assets/reference-configs/document-generation.md +1 -1
  21. package/assets/reference-configs/harness-overview.md +1 -1
  22. package/assets/reference-configs/hook-operations.md +1 -1
  23. package/assets/reference-configs/sprint-contracts.md +3 -3
  24. package/assets/skill-version.json +2 -2
  25. package/assets/templates/contract.template.md +2 -7
  26. package/assets/templates/helpers/capture-plan.sh +2 -2
  27. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  28. package/assets/templates/helpers/contract-worktree.sh +1 -21
  29. package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/new-plan.sh +1 -1
  32. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  33. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  34. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/plan.template.md +1 -1
  38. package/assets/templates/review.template.md +9 -34
  39. package/assets/workflow-contract.v1.json +10 -19
  40. package/docs/README.md +10 -10
  41. package/docs/architecture/CURRENT.md +170 -0
  42. package/docs/architecture/EVOLUTION.md +42 -0
  43. package/docs/architecture/current/README.md +4 -156
  44. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  45. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  46. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  47. package/docs/architecture/history/README.md +7 -0
  48. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  49. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  50. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  51. package/docs/architecture/history.md +3 -34
  52. package/docs/architecture/index.md +15 -98
  53. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  54. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  55. package/docs/architecture/versions/1.6.md +38 -0
  56. package/docs/architecture/versions/1.7.md +41 -0
  57. package/docs/forge-chatgpt-browser-engine.md +2 -2
  58. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  59. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  60. package/docs/forge-github-issue-launcher.md +1 -1
  61. package/docs/forge-runtime-self-healing-loop.md +1 -1
  62. package/docs/operations/controller-browser-plugin.md +38 -22
  63. package/docs/operations/controller-performance-and-502.md +124 -0
  64. package/docs/operations/controller-reliability-runbook.md +1 -1
  65. package/docs/operations/features.md +3 -3
  66. package/docs/operations/github-repository.md +1 -1
  67. package/docs/operations/homebrew.md +2 -2
  68. package/docs/operations/mcp-tool-exposure.md +5 -3
  69. package/docs/tutorials/01-install-and-start.md +1 -1
  70. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Installation.md +1 -1
  74. package/docs/wiki/Integrations.md +2 -2
  75. package/docs/wiki/Quick-Start.md +1 -1
  76. package/docs/wiki/Work-Lifecycle.md +1 -1
  77. package/package.json +29 -10
  78. package/scripts/activate-source-baseline.ts +17 -13
  79. package/scripts/benchmark-background-check-overlap.ts +228 -0
  80. package/scripts/benchmark-mcp-host.sh +117 -0
  81. package/scripts/benchmark-mcp-transport.ts +170 -0
  82. package/scripts/benchmark-quality-harness.ts +146 -0
  83. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  84. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  85. package/scripts/benchmark-typescript-navigation.ts +186 -0
  86. package/scripts/benchmark-work-efficiency.ts +334 -0
  87. package/scripts/capture-plan.sh +3 -3
  88. package/scripts/check-agent-tooling.sh +8 -35
  89. package/scripts/check-architecture-sync.sh +17 -86
  90. package/scripts/check-mcp-compatibility.ts +14 -0
  91. package/scripts/check-public-docs.sh +14 -1
  92. package/scripts/check-runtime-architecture.mjs +107 -64
  93. package/scripts/check-stable-baseline.ts +92 -0
  94. package/scripts/check-task-workflow.sh +4 -4
  95. package/scripts/contract-worktree.sh +1 -21
  96. package/scripts/ensure-task-workflow.sh +22 -55
  97. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  98. package/scripts/install-standalone-recovery.ts +24 -0
  99. package/scripts/lib/project-init-lib.sh +15 -44
  100. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  101. package/scripts/migrate-project-template.sh +23 -1179
  102. package/scripts/new-plan.sh +1 -1
  103. package/scripts/plan-to-todo.sh +11 -36
  104. package/scripts/run-governed-gate.ts +2 -1
  105. package/scripts/ship-worktrees.sh +2 -29
  106. package/scripts/smoke-ios-physical-device.ts +4 -2
  107. package/scripts/smoke-runtime-recovery.ts +1 -35
  108. package/scripts/smoke-schedule-engine.ts +1 -20
  109. package/scripts/sprint-backlog.sh +1 -1
  110. package/scripts/verify-contract.sh +1 -111
  111. package/scripts/verify-forge-runtime.sh +0 -1
  112. package/scripts/verify-sprint.sh +3 -180
  113. package/scripts/workflow-contract.ts +1 -5
  114. package/src/cli/commands/chatgpt.ts +31 -2
  115. package/src/cli/commands/init-hook.ts +3 -3
  116. package/src/cli/commands/mcp.ts +9 -5
  117. package/src/cli/commands/plugin.ts +37 -10
  118. package/src/cli/commands/recovery.ts +25 -3
  119. package/src/cli/commands/runtime.ts +55 -3
  120. package/src/cli/commands/security.ts +12 -1
  121. package/src/cli/commands/setup-profile.ts +31 -21
  122. package/src/cli/controller/check-runner.ts +106 -3
  123. package/src/cli/controller/composite-result.ts +1 -1
  124. package/src/cli/controller/context/focus.ts +51 -0
  125. package/src/cli/controller/context/instruction-context.ts +114 -0
  126. package/src/cli/controller/context/known-paths.ts +76 -0
  127. package/src/cli/controller/context/query-planning.ts +110 -0
  128. package/src/cli/controller/context/rule-resolution.ts +532 -0
  129. package/src/cli/controller/context/source-materializer.ts +278 -0
  130. package/src/cli/controller/context/structural.ts +16 -0
  131. package/src/cli/controller/context/types.ts +286 -0
  132. package/src/cli/controller/context-pack.ts +898 -430
  133. package/src/cli/controller/launch-agents.ts +77 -2
  134. package/src/cli/controller/operational-plan.ts +2 -2
  135. package/src/cli/controller/work-mode.ts +6 -4
  136. package/src/cli/editing/edit-session.ts +60 -7
  137. package/src/cli/github/github.ts +133 -0
  138. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  139. package/src/cli/hook/prompt-intents.ts +1 -1
  140. package/src/cli/installer/managed-entries.ts +15 -1
  141. package/src/cli/local-bridge/console-automations.ts +225 -67
  142. package/src/cli/local-bridge/console-view-models.ts +3 -35
  143. package/src/cli/local-bridge/facade-api.ts +2 -217
  144. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  145. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
  146. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  147. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
  148. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  149. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  150. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
  151. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  152. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  153. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
  154. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  155. package/src/cli/local-bridge/frontend/src/types.ts +8 -7
  156. package/src/cli/local-bridge/job-store.ts +1 -490
  157. package/src/cli/local-bridge/server.ts +29 -532
  158. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  159. package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
  160. package/src/cli/mcp/auth.ts +5 -5
  161. package/src/cli/mcp/instructions.ts +3 -2
  162. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  163. package/src/cli/mcp/multi-repository.ts +16 -5
  164. package/src/cli/mcp/oauth.ts +175 -26
  165. package/src/cli/mcp/paths.ts +8 -2
  166. package/src/cli/mcp/repository-tools.ts +356 -101
  167. package/src/cli/mcp/server.ts +625 -54
  168. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  169. package/src/cli/mcp/toolset-selection.ts +1 -1
  170. package/src/cli/mcp/toolset.ts +42 -5
  171. package/src/cli/mcp/transports/http.ts +125 -15
  172. package/src/cli/mcp/transports/session-registry.ts +67 -5
  173. package/src/cli/mcp/types.ts +3 -3
  174. package/src/cli/repositories/command-classifier.ts +209 -14
  175. package/src/cli/repositories/command-executor.ts +55 -521
  176. package/src/cli/repositories/command-process.ts +108 -0
  177. package/src/cli/repositories/command-scope.ts +99 -41
  178. package/src/cli/repositories/controller-home.ts +7 -8
  179. package/src/cli/repositories/git-command-executor.ts +2 -0
  180. package/src/cli/repositories/locks.ts +53 -16
  181. package/src/cli/repositories/repository-snapshot.ts +121 -0
  182. package/src/cli/repositories/safe-patch.ts +6 -4
  183. package/src/cli/repositories/selected-path-actions.ts +17 -9
  184. package/src/cli/repositories/structured-git.ts +110 -10
  185. package/src/cli/repository/inspector.ts +565 -62
  186. package/src/cli/repository/session-cache.ts +58 -17
  187. package/src/cli/v81-entry.ts +1 -4
  188. package/src/effects/fs-transaction.ts +85 -17
  189. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  190. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  191. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  192. package/src/runtime/context/lsp-client.ts +283 -0
  193. package/src/runtime/context/semantic-navigation.ts +275 -0
  194. package/src/runtime/context/swift-navigation.ts +565 -0
  195. package/src/runtime/context/typescript-navigation.ts +226 -0
  196. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  197. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
  198. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  199. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  200. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  201. package/src/runtime/control-plane/execution/validation.ts +15 -2
  202. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  203. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  204. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  205. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  206. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  207. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
  208. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
  209. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
  210. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  211. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  212. package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
  213. package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
  214. package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
  215. package/src/runtime/control-plane/facade/index.ts +1 -0
  216. package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
  217. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  218. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  219. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  220. package/src/runtime/control-plane/facade/types.ts +55 -11
  221. package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
  222. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  223. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  224. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  225. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  226. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  227. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  228. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  229. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  230. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  231. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  232. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  233. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  234. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  235. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  236. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  237. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  238. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  239. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  240. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  241. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
  242. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  243. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  244. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  245. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  246. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  247. package/src/runtime/control-plane/release-retention.ts +62 -2
  248. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  249. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  250. package/src/runtime/diagnostics/mcp-timing.ts +19 -0
  251. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  252. package/src/runtime/diagnostics/performance.ts +24 -1
  253. package/src/runtime/evidence/event-ledger.ts +1 -1
  254. package/src/runtime/execution/jobs/store.ts +58 -4
  255. package/src/runtime/execution/jobs/types.ts +1 -1
  256. package/src/runtime/execution/managed-workspace.ts +203 -1
  257. package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
  258. package/src/runtime/execution/process-runtime/check-result.ts +63 -2
  259. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  260. package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
  261. package/src/runtime/execution/process-runtime/gc.ts +98 -12
  262. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  263. package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
  264. package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
  265. package/src/runtime/execution/process-runtime/runtime.ts +59 -4
  266. package/src/runtime/execution/process-runtime/store.ts +7 -0
  267. package/src/runtime/execution/process-runtime/types.ts +2 -0
  268. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  269. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  270. package/src/runtime/execution/workers/executor.ts +0 -48
  271. package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
  272. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  273. package/src/runtime/gateway/mcp/router.ts +185 -83
  274. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  275. package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
  276. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  277. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  278. package/src/runtime/plugins/browser-adapter.ts +2925 -318
  279. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  280. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  281. package/src/runtime/plugins/browser-handoff.ts +92 -5
  282. package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
  283. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  284. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  285. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  286. package/src/runtime/plugins/browser-runtime.ts +183 -0
  287. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  288. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  289. package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
  290. package/src/runtime/plugins/external-adapter.ts +324 -3
  291. package/src/runtime/plugins/external-registration.ts +84 -5
  292. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  293. package/src/runtime/plugins/first-party-registry.ts +4 -2
  294. package/src/runtime/plugins/github-adapter.ts +125 -0
  295. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  296. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  297. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  298. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  299. package/src/runtime/plugins/ios-adapter.ts +106 -16
  300. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  301. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  302. package/src/runtime/plugins/lightweight-action.ts +153 -0
  303. package/src/runtime/plugins/local-system-adapter.ts +125 -8
  304. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  305. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  306. package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
  307. package/src/runtime/plugins/resend-adapter.ts +1 -1
  308. package/src/runtime/plugins/store.ts +366 -24
  309. package/src/runtime/plugins/types.ts +39 -2
  310. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  311. package/src/runtime/projections/git-status-sampler.ts +16 -16
  312. package/src/runtime/projections/materialized-view.ts +28 -10
  313. package/src/runtime/recovery/actions.ts +0 -10
  314. package/src/runtime/recovery/classifier.ts +0 -9
  315. package/src/runtime/recovery/maintenance-executor.ts +415 -18
  316. package/src/runtime/recovery/probe.ts +0 -1
  317. package/src/runtime/recovery/types.ts +0 -1
  318. package/src/runtime/root/mcp-transport.ts +180 -10
  319. package/src/runtime/root/package-connector-service.ts +186 -8
  320. package/src/runtime/root/package-runtime-release.ts +143 -17
  321. package/src/runtime/root/package-runtime-service.ts +430 -22
  322. package/src/runtime/root/release-manifest.ts +7 -2
  323. package/src/runtime/root/release-materialize.ts +162 -19
  324. package/src/runtime/root/release-store.ts +48 -11
  325. package/src/runtime/root/runtime.ts +23 -5
  326. package/src/runtime/root/service.ts +100 -27
  327. package/src/runtime/root/types.ts +2 -0
  328. package/src/runtime/safe-tooling/index.ts +0 -1
  329. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  330. package/src/runtime/safe-tooling/types.ts +0 -30
  331. package/src/runtime/shared/trusted-executable.ts +62 -0
  332. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  333. package/src/runtime/standalone-recovery/core.ts +702 -94
  334. package/src/runtime/standalone-recovery/entry.ts +99 -23
  335. package/src/runtime/standalone-recovery/installer.ts +19 -3
  336. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  337. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  338. package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
  339. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  340. package/src/runtime/workflow/schedules/engine.ts +509 -80
  341. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  342. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  343. package/src/runtime/workflow/schedules/store.ts +131 -10
  344. package/src/runtime/workflow/schedules/types.ts +6 -3
  345. package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
  346. package/src/testing/test-governance.ts +8 -40
  347. package/assets/reference-configs/heartbeat-triage.md +0 -57
  348. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  349. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  350. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  351. package/bin/forge-desktop-helper.mjs +0 -204
  352. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  353. package/docs/architecture/current/architecture-invariants.md +0 -331
  354. package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
  355. package/docs/architecture/current/codegraph-context-provider.md +0 -226
  356. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  357. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  358. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
  359. package/docs/architecture/current/entity-model.md +0 -592
  360. package/docs/architecture/current/external-plugin-governance.md +0 -396
  361. package/docs/architecture/current/failure-recovery.md +0 -447
  362. package/docs/architecture/current/governance.md +0 -213
  363. package/docs/architecture/current/human-interaction-plane.md +0 -87
  364. package/docs/architecture/current/implementation-status.md +0 -128
  365. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  366. package/docs/architecture/current/legacy-governance.md +0 -58
  367. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  368. package/docs/architecture/current/migration-roadmap.md +0 -105
  369. package/docs/architecture/current/multi-repository-execution.md +0 -371
  370. package/docs/architecture/current/operations-runbook.md +0 -45
  371. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  372. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  373. package/docs/architecture/current/repository-access-modes.md +0 -102
  374. package/docs/architecture/current/route-policy.md +0 -15
  375. package/docs/architecture/current/route-session-concurrency.md +0 -85
  376. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  377. package/docs/architecture/current/runtime-directory-map.md +0 -49
  378. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  379. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  380. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  381. package/docs/architecture/current/system-overview.md +0 -144
  382. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  383. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  384. package/docs/architecture/current/thin-harness-v1.md +0 -311
  385. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  386. package/docs/forge-autonomous-goal-loop.md +0 -9
  387. package/docs/forge-provider-configuration.md +0 -7
  388. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
  389. package/docs/operations/controller-desktop-plugin.md +0 -112
  390. package/docs/operations/gmail-assistant-routines.md +0 -186
  391. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  392. package/docs/operations/provider-configuration.md +0 -31
  393. package/scripts/harness-trace-grade.sh +0 -150
  394. package/scripts/heartbeat-triage.sh +0 -361
  395. package/scripts/maintenance-triage.sh +0 -167
  396. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  397. package/src/cli/repositories/goal-registry.ts +0 -239
  398. package/src/runtime/assistant/action-proposals.ts +0 -279
  399. package/src/runtime/assistant/intent.ts +0 -395
  400. package/src/runtime/assistant/model-provider.ts +0 -460
  401. package/src/runtime/assistant/openapi.ts +0 -223
  402. package/src/runtime/assistant/policy.ts +0 -89
  403. package/src/runtime/assistant/readiness.ts +0 -258
  404. package/src/runtime/assistant/routine-runtime.ts +0 -477
  405. package/src/runtime/assistant/schedule-binding.ts +0 -171
  406. package/src/runtime/assistant/standing-grants.ts +0 -375
  407. package/src/runtime/assistant/store.ts +0 -243
  408. package/src/runtime/assistant/types.ts +0 -135
  409. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  410. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  411. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  412. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  413. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  414. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  415. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  416. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  417. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  418. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  419. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  420. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  421. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  422. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  423. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  424. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  425. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  426. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  427. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  428. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  429. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
  430. package/src/runtime/workflow/findings/store.ts +0 -204
  431. package/src/runtime/workflow/findings/types.ts +0 -58
  432. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  433. package/src/runtime/workflow/portfolio/store.ts +0 -185
  434. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -1,12 +1,15 @@
1
1
  import { createHash } from 'crypto';
2
- import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'fs';
3
- import { join } from 'path';
2
+ import { execFile } from 'child_process';
3
+ 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';
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[];
@@ -264,12 +395,26 @@ export interface SearchRepositoryOptions {
264
395
 
265
396
  export interface SearchRepositoryManyOptions {
266
397
  queries: string[];
398
+ /** Pre-resolved exact candidate files. Skips repository inventory discovery. */
399
+ files?: string[];
267
400
  includeGlobs?: string[];
268
401
  excludeGlobs?: string[];
269
402
  maxResultsPerQuery?: number;
270
403
  maxFiles?: number;
271
404
  caseSensitive?: boolean;
272
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;
273
418
  }
274
419
 
275
420
  export interface SearchRepositoryManyResult {
@@ -280,10 +425,76 @@ export interface SearchRepositoryManyResult {
280
425
  skippedLargeFiles: number;
281
426
  skippedBinaryFiles: number;
282
427
  truncated: boolean;
283
- truncationReason?: 'max_results' | 'max_files';
428
+ truncationReason?: 'max_results' | 'max_files' | 'discovery_budget';
284
429
  cacheHit?: boolean;
285
430
  }
286
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
+
287
498
  function searchInventory(
288
499
  repoRoot: string,
289
500
  includes: string[],
@@ -311,6 +522,157 @@ function searchInventory(
311
522
  return { files: candidates.slice(0, maxFiles), candidateCount: candidates.length, cacheHit };
312
523
  }
313
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
+
314
676
  /**
315
677
  * Internal Context Plane batch search. Every candidate file is policy-checked
316
678
  * and read at most once while all lexical terms are matched in the same pass.
@@ -321,24 +683,52 @@ export function searchRepositoryMany(
321
683
  policy: McpPolicy,
322
684
  opts: SearchRepositoryManyOptions,
323
685
  ): SearchRepositoryManyResult {
324
- const queries = Array.from(new Set(opts.queries.map((query) => query.trim()).filter(Boolean)));
325
- if (queries.length === 0) throw new Error('at least one search query is required');
326
- const maxResultsPerQuery = Math.min(Math.max(opts.maxResultsPerQuery ?? 100, 1), 500);
327
- const maxFiles = Math.min(Math.max(opts.maxFiles ?? 5000, 1), 20_000);
328
- const includes = opts.includeGlobs ?? [];
329
- const excludes = [...(includes.length === 0 ? DEFAULT_EXCLUDES : []), ...(opts.excludeGlobs ?? [])];
330
- const inventory = searchInventory(repoRoot, includes, excludes, maxFiles, opts.cacheKey);
331
- const needles = queries.map((query) => ({ query, needle: opts.caseSensitive ? query : query.toLowerCase() }));
332
- const counts = new Map(queries.map((query) => [query, 0]));
333
- const results: SearchRepositoryManyResult['results'] = [];
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
+ }
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);
714
+ const matchState = createSearchManyMatchState(
715
+ queries,
716
+ maxResultsPerQuery,
717
+ completionMode,
718
+ discoveryTargetFiles,
719
+ discoveryMinQueryCoverage,
720
+ requiredQueries,
721
+ opts.caseSensitive === true,
722
+ );
334
723
  let scannedFiles = 0;
335
724
  let policyDeniedFiles = 0;
336
725
  let skippedLargeFiles = 0;
337
726
  let skippedBinaryFiles = 0;
727
+ const canonicalRepoRoot = realpathSync(repoRoot);
338
728
 
339
729
  for (const path of inventory.files) {
340
- if (needles.every(({ query }) => (counts.get(query) ?? 0) >= maxResultsPerQuery)) break;
341
- const decision = resolveMcpPath(repoRoot, path, policy, 'read');
730
+ if (searchManyComplete(matchState)) break;
731
+ const decision = resolveMcpPath(repoRoot, path, policy, 'read', canonicalRepoRoot);
342
732
  if (!decision.ok || !decision.absolutePath) {
343
733
  policyDeniedFiles += 1;
344
734
  continue;
@@ -354,46 +744,159 @@ export function searchRepositoryMany(
354
744
  continue;
355
745
  }
356
746
  scannedFiles += 1;
357
- const lines = bytes.toString('utf-8').split(/\r?\n/);
358
- for (let index = 0; index < lines.length; index += 1) {
359
- const text = lines[index]!;
360
- const haystack = opts.caseSensitive ? text : text.toLowerCase();
361
- for (const { query, needle } of needles) {
362
- if ((counts.get(query) ?? 0) >= maxResultsPerQuery || !haystack.includes(needle)) continue;
363
- results.push({ query, path, line: index + 1, text: text.slice(0, 500) });
364
- counts.set(query, (counts.get(query) ?? 0) + 1);
365
- }
366
- }
747
+ matchSearchManySource(matchState, path, bytes);
367
748
  }
368
749
 
369
750
  // Consumers may bound distinct candidate files after this call. Preserve the
370
751
  // caller's query priority so an exact symbol/phrase cannot be displaced by
371
752
  // earlier alphabetic files that only matched broad recall tokens.
372
- const queryPriority = new Map(queries.map((query, index) => [query, index]));
373
- results.sort((left, right) => {
374
- const priority = (queryPriority.get(left.query) ?? queries.length) - (queryPriority.get(right.query) ?? queries.length);
375
- if (priority !== 0) return priority;
376
- const pathOrder = left.path.localeCompare(right.path);
377
- 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,
378
762
  });
763
+ if (sessionCache) {
764
+ sessionCache.putSearch({
765
+ query: batchQueryKey,
766
+ includeKey,
767
+ result: payload,
768
+ scannedFiles,
769
+ });
770
+ }
771
+ return payload;
772
+ }
379
773
 
380
- const hitResultLimit = needles.some(({ query }) => (counts.get(query) ?? 0) >= maxResultsPerQuery);
381
- const truncationReason = hitResultLimit
382
- ? 'max_results' as const
383
- : inventory.candidateCount > inventory.files.length
384
- ? 'max_files' as const
385
- : undefined;
386
- 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 {
387
817
  queries,
388
- 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,
389
883
  scannedFiles,
390
884
  policyDeniedFiles,
391
885
  skippedLargeFiles,
392
886
  skippedBinaryFiles,
393
- truncated: Boolean(truncationReason),
394
- truncationReason,
395
- cacheHit: inventory.cacheHit,
396
- };
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;
397
900
  }
398
901
 
399
902
  export function searchRepository(repoRoot: string, policy: McpPolicy, opts: SearchRepositoryOptions & {