@moretea-labs/forge 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +39 -0
  3. package/CLAUDE.md +4 -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/02-operating-mode.partial.md +2 -0
  17. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  18. package/assets/plugin-registry.v1.json +3 -3
  19. package/assets/reference-configs/document-generation.md +1 -1
  20. package/assets/reference-configs/harness-overview.md +1 -1
  21. package/assets/reference-configs/sprint-contracts.md +3 -3
  22. package/assets/skill-version.json +2 -2
  23. package/assets/templates/contract.template.md +2 -7
  24. package/assets/templates/helpers/capture-plan.sh +2 -2
  25. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  26. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  27. package/assets/templates/helpers/contract-worktree.sh +1 -21
  28. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  29. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  32. package/assets/templates/helpers/new-plan.sh +1 -1
  33. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  34. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/helpers/workflow-contract.ts +5 -1
  38. package/assets/templates/plan.template.md +1 -1
  39. package/assets/templates/review.template.md +9 -34
  40. package/assets/workflow-contract.v1.json +10 -19
  41. package/docs/README.md +10 -10
  42. package/docs/architecture/CURRENT.md +178 -0
  43. package/docs/architecture/EVOLUTION.md +42 -0
  44. package/docs/architecture/current/README.md +4 -156
  45. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  46. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  47. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  48. package/docs/architecture/history/README.md +7 -0
  49. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  50. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  51. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  52. package/docs/architecture/history.md +3 -34
  53. package/docs/architecture/index.md +15 -98
  54. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  55. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  56. package/docs/architecture/versions/1.6.md +38 -0
  57. package/docs/architecture/versions/1.7.md +41 -0
  58. package/docs/forge-chatgpt-browser-engine.md +2 -2
  59. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  60. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  61. package/docs/forge-github-issue-launcher.md +1 -1
  62. package/docs/forge-plugin-management.md +1 -1
  63. package/docs/forge-runtime-self-healing-loop.md +1 -1
  64. package/docs/operations/controller-browser-plugin.md +26 -6
  65. package/docs/operations/controller-performance-and-502.md +143 -0
  66. package/docs/operations/controller-reliability-runbook.md +1 -1
  67. package/docs/operations/features.md +2 -2
  68. package/docs/operations/github-repository.md +1 -1
  69. package/docs/operations/homebrew.md +2 -2
  70. package/docs/operations/mcp-tool-exposure.md +5 -3
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Integrations.md +2 -2
  74. package/docs/wiki/Work-Lifecycle.md +1 -1
  75. package/package.json +33 -11
  76. package/scripts/activate-source-baseline.ts +17 -13
  77. package/scripts/architecture-event.ts +2 -820
  78. package/scripts/architecture-queue.sh +2 -645
  79. package/scripts/archive-architecture-request.sh +2 -242
  80. package/scripts/archive-workflow.sh +1 -291
  81. package/scripts/benchmark-background-check-overlap.ts +228 -0
  82. package/scripts/benchmark-mcp-host.sh +117 -0
  83. package/scripts/benchmark-mcp-transport.ts +170 -0
  84. package/scripts/benchmark-quality-harness.ts +146 -0
  85. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  86. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  87. package/scripts/benchmark-typescript-navigation.ts +186 -0
  88. package/scripts/benchmark-work-efficiency.ts +334 -0
  89. package/scripts/capability-config.ts +2 -392
  90. package/scripts/capability-resolver.ts +2 -503
  91. package/scripts/capture-plan.sh +1 -332
  92. package/scripts/check-agent-tooling.sh +2 -1558
  93. package/scripts/check-architecture-sync.sh +2 -371
  94. package/scripts/check-brain-manifest.sh +2 -253
  95. package/scripts/check-context-files.sh +2 -47
  96. package/scripts/check-deploy-sql-order.sh +2 -115
  97. package/scripts/check-mcp-compatibility.ts +14 -0
  98. package/scripts/check-runtime-architecture.mjs +349 -79
  99. package/scripts/check-skill-version.ts +3 -191
  100. package/scripts/check-source-duplication.mjs +61 -0
  101. package/scripts/check-stable-baseline.ts +92 -0
  102. package/scripts/check-task-sync.sh +2 -63
  103. package/scripts/check-task-workflow.sh +2 -1000
  104. package/scripts/codex-handoff-resume.sh +2 -267
  105. package/scripts/context-contract-sync.sh +2 -514
  106. package/scripts/contract-run.ts +2 -463
  107. package/scripts/contract-worktree.sh +1 -722
  108. package/scripts/ensure-task-workflow.sh +1 -1225
  109. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  110. package/scripts/inspect-project-state.ts +4 -279
  111. package/scripts/install-standalone-recovery.ts +24 -0
  112. package/scripts/lib/project-init-lib.sh +22 -46
  113. package/scripts/lib/run-forge-helper.sh +19 -0
  114. package/scripts/lib/run-forge-helper.ts +26 -0
  115. package/scripts/migrate-project-template.sh +1 -1189
  116. package/scripts/migrate-workflow-docs.ts +4 -412
  117. package/scripts/new-plan.sh +1 -180
  118. package/scripts/new-spec.sh +1 -47
  119. package/scripts/new-sprint.sh +1 -46
  120. package/scripts/plan-to-todo.sh +1 -796
  121. package/scripts/prepare-codex-handoff.sh +2 -201
  122. package/scripts/prepare-handoff.sh +1 -78
  123. package/scripts/refresh-current-status.sh +2 -467
  124. package/scripts/run-governed-gate.ts +5 -1
  125. package/scripts/select-agent-context-blocks.sh +2 -64
  126. package/scripts/ship-worktrees.sh +1 -558
  127. package/scripts/smoke-ios-physical-device.ts +4 -2
  128. package/scripts/smoke-runtime-recovery.ts +1 -35
  129. package/scripts/smoke-schedule-engine.ts +1 -20
  130. package/scripts/sprint-backlog.sh +1 -834
  131. package/scripts/stage-runtime-release.ts +2 -1
  132. package/scripts/summarize-failures.sh +2 -123
  133. package/scripts/switch-plan.sh +1 -150
  134. package/scripts/sync-brain-docs.sh +2 -379
  135. package/scripts/sync-generated-authority.mjs +175 -0
  136. package/scripts/verify-contract.sh +2 -703
  137. package/scripts/verify-forge-runtime.sh +0 -1
  138. package/scripts/verify-sprint.sh +1 -677
  139. package/scripts/workflow-contract.ts +4 -205
  140. package/scripts/workstream-sync.sh +2 -261
  141. package/src/cli/agent-jobs/executor-health.ts +1 -1
  142. package/src/cli/commands/init-hook.ts +3 -3
  143. package/src/cli/commands/mcp.ts +9 -5
  144. package/src/cli/commands/plugin.ts +12 -3
  145. package/src/cli/commands/recovery.ts +25 -3
  146. package/src/cli/commands/runtime.ts +55 -3
  147. package/src/cli/commands/security.ts +12 -1
  148. package/src/cli/commands/setup-profile.ts +31 -21
  149. package/src/cli/controller/check-runner.ts +113 -5
  150. package/src/cli/controller/composite-result.ts +1 -1
  151. package/src/cli/controller/context/focus.ts +51 -0
  152. package/src/cli/controller/context/instruction-context.ts +114 -0
  153. package/src/cli/controller/context/known-paths.ts +76 -0
  154. package/src/cli/controller/context/query-planning.ts +110 -0
  155. package/src/cli/controller/context/rule-resolution.ts +532 -0
  156. package/src/cli/controller/context/source-materializer.ts +278 -0
  157. package/src/cli/controller/context/structural.ts +16 -0
  158. package/src/cli/controller/context/types.ts +286 -0
  159. package/src/cli/controller/context-pack.ts +828 -487
  160. package/src/cli/controller/launch-agents.ts +77 -2
  161. package/src/cli/controller/operational-plan.ts +2 -2
  162. package/src/cli/controller/work-mode.ts +6 -4
  163. package/src/cli/editing/edit-session.ts +60 -7
  164. package/src/cli/github/contracts.ts +16 -0
  165. package/src/cli/github/github.ts +135 -16
  166. package/src/cli/github/session.ts +2 -1
  167. package/src/cli/installer/managed-entries.ts +15 -1
  168. package/src/cli/local-bridge/console-automations.ts +54 -78
  169. package/src/cli/local-bridge/console-view-models.ts +1 -35
  170. package/src/cli/local-bridge/facade-api.ts +37 -264
  171. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  172. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  173. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  174. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  175. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  176. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  177. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  178. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  179. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  180. package/src/cli/local-bridge/job-store.ts +1 -490
  181. package/src/cli/local-bridge/server.ts +2 -515
  182. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  183. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  184. package/src/cli/mcp/auth.ts +5 -5
  185. package/src/cli/mcp/instructions.ts +3 -2
  186. package/src/cli/mcp/legacy-tool-service.ts +10 -222
  187. package/src/cli/mcp/multi-repository.ts +16 -5
  188. package/src/cli/mcp/oauth.ts +175 -26
  189. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  190. package/src/cli/mcp/repository-tools.ts +282 -146
  191. package/src/cli/mcp/server.ts +586 -116
  192. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  193. package/src/cli/mcp/tool-contract.ts +14 -0
  194. package/src/cli/mcp/toolset-selection.ts +1 -1
  195. package/src/cli/mcp/toolset.ts +44 -7
  196. package/src/cli/mcp/transports/http.ts +124 -14
  197. package/src/cli/mcp/transports/session-registry.ts +43 -5
  198. package/src/cli/mcp/types.ts +3 -3
  199. package/src/cli/repositories/command-classifier.ts +103 -21
  200. package/src/cli/repositories/command-executor.ts +78 -526
  201. package/src/cli/repositories/command-process.ts +108 -0
  202. package/src/cli/repositories/command-scope.ts +99 -41
  203. package/src/cli/repositories/controller-home.ts +7 -8
  204. package/src/cli/repositories/git-command-executor.ts +2 -0
  205. package/src/cli/repositories/locks.ts +53 -16
  206. package/src/cli/repositories/repository-snapshot.ts +124 -0
  207. package/src/cli/repositories/selected-path-actions.ts +17 -9
  208. package/src/cli/repositories/structured-git.ts +118 -6
  209. package/src/cli/repository/inspector.ts +563 -65
  210. package/src/cli/repository/session-cache.ts +58 -17
  211. package/src/cli/v81-entry.ts +1 -4
  212. package/src/effects/fs-transaction.ts +85 -17
  213. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  214. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  215. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  216. package/src/runtime/context/lsp-client.ts +283 -0
  217. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  218. package/src/runtime/context/semantic-navigation.ts +223 -0
  219. package/src/runtime/context/swift-navigation.ts +565 -0
  220. package/src/runtime/context/typescript-navigation.ts +226 -0
  221. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  222. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
  223. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  224. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  225. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  226. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  227. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  228. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  229. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  230. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  231. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  232. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  233. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  234. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  235. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  236. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  237. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  238. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  239. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  240. package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
  241. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
  242. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  243. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  244. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  245. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
  246. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  247. package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
  248. package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
  249. package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
  250. package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
  251. package/src/runtime/control-plane/facade/index.ts +3 -0
  252. package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
  253. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  254. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  255. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  256. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  257. package/src/runtime/control-plane/facade/types.ts +90 -10
  258. package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
  259. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  260. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  261. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  262. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  263. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  264. package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
  265. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  266. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  267. package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
  268. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  269. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  270. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  271. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  272. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  273. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  274. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  275. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  276. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  277. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  278. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
  279. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
  280. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
  281. package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
  282. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  283. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  284. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  285. package/src/runtime/control-plane/release-retention.ts +62 -2
  286. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  287. package/src/runtime/control-plane/routing/route-policy.ts +24 -21
  288. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  289. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  290. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  291. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  292. package/src/runtime/diagnostics/performance.ts +24 -1
  293. package/src/runtime/evidence/event-ledger.ts +1 -1
  294. package/src/runtime/evidence/process-check-execution.ts +18 -0
  295. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  296. package/src/runtime/execution/jobs/store.ts +58 -4
  297. package/src/runtime/execution/jobs/types.ts +1 -1
  298. package/src/runtime/execution/managed-workspace.ts +203 -1
  299. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  300. package/src/runtime/execution/process-runtime/check-result.ts +88 -0
  301. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  302. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  303. package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
  304. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  305. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
  306. package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
  307. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  308. package/src/runtime/execution/process-runtime/runtime.ts +53 -3
  309. package/src/runtime/execution/process-runtime/store.ts +1 -0
  310. package/src/runtime/execution/process-runtime/types.ts +4 -12
  311. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  312. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  313. package/src/runtime/execution/work-activity.ts +21 -0
  314. package/src/runtime/execution/workers/executor.ts +18 -61
  315. package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
  316. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  317. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  318. package/src/runtime/gateway/mcp/router.ts +186 -84
  319. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
  320. package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
  321. package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
  322. package/src/runtime/plugins/browser-adapter.ts +2662 -829
  323. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  324. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  325. package/src/runtime/plugins/browser-handoff.ts +110 -5
  326. package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
  327. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  328. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  329. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  330. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  331. package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
  332. package/src/runtime/plugins/browser-runtime.ts +193 -0
  333. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  334. package/src/runtime/plugins/browser-session-store.ts +121 -0
  335. package/src/runtime/plugins/browser-session-types.ts +98 -0
  336. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  337. package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
  338. package/src/runtime/plugins/external-adapter.ts +358 -3
  339. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  340. package/src/runtime/plugins/first-party-registry.ts +2 -0
  341. package/src/runtime/plugins/github-adapter.ts +125 -0
  342. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  343. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  344. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  345. package/src/runtime/plugins/ios-adapter.ts +106 -16
  346. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  347. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  348. package/src/runtime/plugins/lightweight-action.ts +153 -0
  349. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  350. package/src/runtime/plugins/macos-capability-broker.ts +47 -9
  351. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  352. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  353. package/src/runtime/plugins/resend-adapter.ts +1 -1
  354. package/src/runtime/plugins/store.ts +330 -33
  355. package/src/runtime/plugins/types.ts +38 -3
  356. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  357. package/src/runtime/projections/git-status-sampler.ts +16 -16
  358. package/src/runtime/projections/materialized-view.ts +28 -10
  359. package/src/runtime/recovery/maintenance-executor.ts +426 -25
  360. package/src/runtime/root/mcp-transport.ts +180 -10
  361. package/src/runtime/root/package-connector-service.ts +230 -10
  362. package/src/runtime/root/package-runtime-release.ts +143 -17
  363. package/src/runtime/root/package-runtime-service.ts +461 -22
  364. package/src/runtime/root/release-manifest.ts +144 -18
  365. package/src/runtime/root/release-materialize.ts +125 -32
  366. package/src/runtime/root/release-store.ts +48 -11
  367. package/src/runtime/root/runtime.ts +23 -5
  368. package/src/runtime/root/service.ts +100 -29
  369. package/src/runtime/root/types.ts +12 -0
  370. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  371. package/src/runtime/shared/trusted-executable.ts +62 -0
  372. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  373. package/src/runtime/standalone-recovery/core.ts +1079 -240
  374. package/src/runtime/standalone-recovery/entry.ts +100 -24
  375. package/src/runtime/standalone-recovery/installer.ts +20 -4
  376. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  377. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  378. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  379. package/src/runtime/workflow/schedules/engine.ts +364 -202
  380. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  381. package/src/runtime/workflow/schedules/settlement.ts +64 -17
  382. package/src/runtime/workflow/schedules/store.ts +209 -19
  383. package/src/runtime/workflow/schedules/types.ts +2 -4
  384. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  385. package/src/testing/test-governance.ts +8 -40
  386. package/assets/reference-configs/heartbeat-triage.md +0 -57
  387. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  388. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  389. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  390. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  391. package/docs/architecture/current/architecture-invariants.md +0 -331
  392. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  393. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  394. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  395. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  396. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  397. package/docs/architecture/current/entity-model.md +0 -612
  398. package/docs/architecture/current/external-plugin-governance.md +0 -396
  399. package/docs/architecture/current/failure-recovery.md +0 -447
  400. package/docs/architecture/current/governance.md +0 -213
  401. package/docs/architecture/current/human-interaction-plane.md +0 -87
  402. package/docs/architecture/current/implementation-status.md +0 -128
  403. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  404. package/docs/architecture/current/legacy-governance.md +0 -58
  405. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  406. package/docs/architecture/current/migration-roadmap.md +0 -105
  407. package/docs/architecture/current/multi-repository-execution.md +0 -371
  408. package/docs/architecture/current/operations-runbook.md +0 -45
  409. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  410. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  411. package/docs/architecture/current/repository-access-modes.md +0 -102
  412. package/docs/architecture/current/route-policy.md +0 -15
  413. package/docs/architecture/current/route-session-concurrency.md +0 -85
  414. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  415. package/docs/architecture/current/runtime-directory-map.md +0 -49
  416. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  417. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  418. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  419. package/docs/architecture/current/system-overview.md +0 -169
  420. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  421. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  422. package/docs/architecture/current/thin-harness-v1.md +0 -321
  423. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  424. package/docs/forge-autonomous-goal-loop.md +0 -9
  425. package/docs/forge-provider-configuration.md +0 -7
  426. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  427. package/docs/operations/gmail-assistant-routines.md +0 -186
  428. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  429. package/docs/operations/provider-configuration.md +0 -31
  430. package/scripts/harness-trace-grade.sh +0 -150
  431. package/scripts/heartbeat-triage.sh +0 -361
  432. package/scripts/maintenance-triage.sh +0 -167
  433. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  434. package/src/cli/repositories/goal-registry.ts +0 -239
  435. package/src/runtime/assistant/action-proposals.ts +0 -279
  436. package/src/runtime/assistant/intent.ts +0 -395
  437. package/src/runtime/assistant/model-provider.ts +0 -460
  438. package/src/runtime/assistant/openapi.ts +0 -223
  439. package/src/runtime/assistant/policy.ts +0 -89
  440. package/src/runtime/assistant/readiness.ts +0 -258
  441. package/src/runtime/assistant/routine-runtime.ts +0 -477
  442. package/src/runtime/assistant/schedule-binding.ts +0 -171
  443. package/src/runtime/assistant/standing-grants.ts +0 -375
  444. package/src/runtime/assistant/store.ts +0 -243
  445. package/src/runtime/assistant/types.ts +0 -135
  446. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  447. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  448. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  449. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  450. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  451. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  452. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  453. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  454. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  455. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  456. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  457. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  458. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  459. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  460. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  461. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  462. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  463. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  464. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  465. package/src/runtime/workflow/findings/store.ts +0 -204
  466. package/src/runtime/workflow/findings/types.ts +0 -58
  467. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  468. package/src/runtime/workflow/portfolio/store.ts +0 -185
  469. 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,13 +727,15 @@ 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;
357
734
  continue;
358
735
  }
359
- const size = statSync(decision.absolutePath).size;
736
+ const info = statSync(decision.absolutePath);
737
+ if (!info.isFile()) continue;
738
+ const size = info.size;
360
739
  if (size > policy.maxFileBytes) {
361
740
  skippedLargeFiles += 1;
362
741
  continue;
@@ -367,46 +746,163 @@ export function searchRepositoryMany(
367
746
  continue;
368
747
  }
369
748
  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
- }
749
+ matchSearchManySource(matchState, path, bytes);
380
750
  }
381
751
 
382
752
  // Consumers may bound distinct candidate files after this call. Preserve the
383
753
  // caller's query priority so an exact symbol/phrase cannot be displaced by
384
754
  // 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;
755
+ const payload = finalizeSearchManyResult({
756
+ state: matchState,
757
+ scannedFiles,
758
+ policyDeniedFiles,
759
+ skippedLargeFiles,
760
+ skippedBinaryFiles,
761
+ candidateCount: inventory.candidateCount,
762
+ inventoryFileCount: inventory.files.length,
763
+ inventoryCacheHit: inventory.cacheHit,
391
764
  });
765
+ if (sessionCache) {
766
+ sessionCache.putSearch({
767
+ query: batchQueryKey,
768
+ includeKey,
769
+ result: payload,
770
+ scannedFiles,
771
+ });
772
+ }
773
+ return payload;
774
+ }
392
775
 
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 {
776
+ // File reads are already bounded by policy/maxFiles. A wider batch keeps broad
777
+ // Context scans from serializing dozens of filesystem waves while still
778
+ // preserving original-order matching and early-stop semantics below.
779
+ const ASYNC_SEARCH_READ_BATCH_SIZE = 64;
780
+
781
+ type AsyncSearchCandidate =
782
+ | { path: string; kind: 'denied' }
783
+ | { path: string; kind: 'large' }
784
+ | { path: string; kind: 'binary' }
785
+ | { path: string; kind: 'nonfile' }
786
+ | { path: string; kind: 'source'; bytes: Buffer }
787
+ | { path: string; kind: 'error'; error: unknown };
788
+
789
+ async function readSearchCandidateAsync(
790
+ repoRoot: string,
791
+ policy: McpPolicy,
792
+ canonicalRepoRoot: string,
793
+ path: string,
794
+ ): Promise<AsyncSearchCandidate> {
795
+ try {
796
+ const decision = resolveMcpPath(repoRoot, path, policy, 'read', canonicalRepoRoot);
797
+ if (!decision.ok || !decision.absolutePath) return { path, kind: 'denied' };
798
+ const info = await statAsync(decision.absolutePath);
799
+ if (!info.isFile()) return { path, kind: 'nonfile' };
800
+ const size = info.size;
801
+ if (size > policy.maxFileBytes) return { path, kind: 'large' };
802
+ const bytes = await readFileAsync(decision.absolutePath);
803
+ if (binary(bytes)) return { path, kind: 'binary' };
804
+ return { path, kind: 'source', bytes };
805
+ } catch (error) {
806
+ return { path, kind: 'error', error };
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Async Context Plane batch search with result semantics matching
812
+ * searchRepositoryMany. Candidate reads happen in bounded batches, while policy
813
+ * accounting, early-stop behavior, matching, and result ordering are consumed in
814
+ * original repository order on the caller thread.
815
+ */
816
+ export async function searchRepositoryManyAsync(
817
+ repoRoot: string,
818
+ policy: McpPolicy,
819
+ opts: SearchRepositoryManyOptions,
820
+ ): Promise<SearchRepositoryManyResult> {
821
+ const {
400
822
  queries,
401
- results,
823
+ maxResultsPerQuery,
824
+ maxFiles,
825
+ includes,
826
+ excludes,
827
+ directCandidates,
828
+ completionMode,
829
+ discoveryTargetFiles,
830
+ discoveryMinQueryCoverage,
831
+ requiredQueries,
832
+ includeKey,
833
+ batchQueryKey,
834
+ } = searchRepositoryManyCacheIdentity(opts);
835
+ const sessionCache = bindSessionCache(repoRoot, opts.session);
836
+ if (sessionCache) {
837
+ const cached = sessionCache.getSearch(batchQueryKey, includeKey);
838
+ if (cached?.result && typeof cached.result === 'object') {
839
+ return { ...(cached.result as SearchRepositoryManyResult), cacheHit: true };
840
+ }
841
+ }
842
+ const inventory = directCandidates && directCandidates.length > 0
843
+ ? {
844
+ files: directCandidates.slice(0, maxFiles),
845
+ candidateCount: directCandidates.length,
846
+ cacheHit: false,
847
+ }
848
+ : await searchInventoryAsync(repoRoot, includes, excludes, maxFiles, opts.cacheKey);
849
+ const matchState = createSearchManyMatchState(
850
+ queries,
851
+ maxResultsPerQuery,
852
+ completionMode,
853
+ discoveryTargetFiles,
854
+ discoveryMinQueryCoverage,
855
+ requiredQueries,
856
+ opts.caseSensitive === true,
857
+ );
858
+ let scannedFiles = 0;
859
+ let policyDeniedFiles = 0;
860
+ let skippedLargeFiles = 0;
861
+ let skippedBinaryFiles = 0;
862
+ const canonicalRepoRoot = realpathSync(repoRoot);
863
+ for (let offset = 0; offset < inventory.files.length && !searchManyComplete(matchState); offset += ASYNC_SEARCH_READ_BATCH_SIZE) {
864
+ const batchPaths = inventory.files.slice(offset, offset + ASYNC_SEARCH_READ_BATCH_SIZE);
865
+ const batch = await Promise.all(batchPaths.map((path) => readSearchCandidateAsync(repoRoot, policy, canonicalRepoRoot, path)));
866
+ for (const candidate of batch) {
867
+ if (searchManyComplete(matchState)) break;
868
+ if (candidate.kind === 'denied') {
869
+ policyDeniedFiles += 1;
870
+ continue;
871
+ }
872
+ if (candidate.kind === 'large') {
873
+ skippedLargeFiles += 1;
874
+ continue;
875
+ }
876
+ if (candidate.kind === 'binary') {
877
+ skippedBinaryFiles += 1;
878
+ continue;
879
+ }
880
+ if (candidate.kind === 'nonfile') continue;
881
+ if (candidate.kind === 'error') throw candidate.error;
882
+ scannedFiles += 1;
883
+ matchSearchManySource(matchState, candidate.path, candidate.bytes);
884
+ }
885
+ }
886
+
887
+ const payload = finalizeSearchManyResult({
888
+ state: matchState,
402
889
  scannedFiles,
403
890
  policyDeniedFiles,
404
891
  skippedLargeFiles,
405
892
  skippedBinaryFiles,
406
- truncated: Boolean(truncationReason),
407
- truncationReason,
408
- cacheHit: inventory.cacheHit,
409
- };
893
+ candidateCount: inventory.candidateCount,
894
+ inventoryFileCount: inventory.files.length,
895
+ inventoryCacheHit: inventory.cacheHit,
896
+ });
897
+ if (sessionCache) {
898
+ sessionCache.putSearch({
899
+ query: batchQueryKey,
900
+ includeKey,
901
+ result: payload,
902
+ scannedFiles,
903
+ });
904
+ }
905
+ return payload;
410
906
  }
411
907
 
412
908
  export function searchRepository(repoRoot: string, policy: McpPolicy, opts: SearchRepositoryOptions & {
@@ -460,7 +956,9 @@ export function searchRepository(repoRoot: string, policy: McpPolicy, opts: Sear
460
956
  policyDeniedFiles += 1;
461
957
  continue;
462
958
  }
463
- const size = statSync(decision.absolutePath).size;
959
+ const info = statSync(decision.absolutePath);
960
+ if (!info.isFile()) continue;
961
+ const size = info.size;
464
962
  if (size > policy.maxFileBytes) {
465
963
  skippedLargeFiles += 1;
466
964
  continue;