@moretea-labs/forge 1.5.0-rc.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 (903) hide show
  1. package/AGENTS.md +120 -0
  2. package/CHANGELOG.md +36 -0
  3. package/CLAUDE.md +120 -0
  4. package/CODE_OF_CONDUCT.md +23 -0
  5. package/CONTRIBUTING.md +77 -0
  6. package/LICENSE +22 -0
  7. package/NOTICE +9 -0
  8. package/README.en.md +5 -0
  9. package/README.es.md +12 -0
  10. package/README.fr.md +12 -0
  11. package/README.ja.md +12 -0
  12. package/README.md +132 -0
  13. package/README.zh-CN.md +84 -0
  14. package/SECURITY.md +50 -0
  15. package/SKILL.md +431 -0
  16. package/SUPPORT.md +31 -0
  17. package/THIRD_PARTY_NOTICES.md +18 -0
  18. package/agents/openai.yaml +4 -0
  19. package/assets/AGENTS.md +68 -0
  20. package/assets/CLAUDE.md +68 -0
  21. package/assets/branding/README.md +10 -0
  22. package/assets/branding/forge-controller-flow.mmd +8 -0
  23. package/assets/hooks/AGENTS.md +75 -0
  24. package/assets/hooks/CLAUDE.md +75 -0
  25. package/assets/hooks/anti-simplification.sh +12 -0
  26. package/assets/hooks/changelog-guard.sh +80 -0
  27. package/assets/hooks/codex.hooks.template.json +60 -0
  28. package/assets/hooks/first-principles-guard.sh +72 -0
  29. package/assets/hooks/hook-input.sh +618 -0
  30. package/assets/hooks/lib/session-state.sh +106 -0
  31. package/assets/hooks/lib/workflow-state.sh +1730 -0
  32. package/assets/hooks/post-bash.sh +215 -0
  33. package/assets/hooks/post-edit-guard.sh +256 -0
  34. package/assets/hooks/post-tool-observer.sh +99 -0
  35. package/assets/hooks/pre-edit-guard.sh +240 -0
  36. package/assets/hooks/prompt-guard.sh +1077 -0
  37. package/assets/hooks/run-hook.sh +67 -0
  38. package/assets/hooks/security-sentinel.sh +115 -0
  39. package/assets/hooks/session-start-context.sh +593 -0
  40. package/assets/hooks/settings.template.json +60 -0
  41. package/assets/hooks/stop-orchestrator.sh +171 -0
  42. package/assets/hooks/subagent-return-channel-guard.sh +107 -0
  43. package/assets/hooks/worktree-guard.sh +39 -0
  44. package/assets/initializer-question-pack.v1.json +132 -0
  45. package/assets/initializer-question-pack.v1.schema.json +46 -0
  46. package/assets/initializer-question-pack.v2.json +174 -0
  47. package/assets/initializer-question-pack.v3.json +206 -0
  48. package/assets/initializer-question-pack.v3.schema.json +92 -0
  49. package/assets/initializer-question-pack.v4.json +557 -0
  50. package/assets/initializer-question-pack.v4.schema.json +169 -0
  51. package/assets/partials/01-header.partial.md +10 -0
  52. package/assets/partials/02-iron-rules.partial.md +30 -0
  53. package/assets/partials/03-philosophy.partial.md +32 -0
  54. package/assets/partials/04-project-structure.partial.md +56 -0
  55. package/assets/partials/05-workflow.partial.md +79 -0
  56. package/assets/partials/06-cloudflare.partial.md +20 -0
  57. package/assets/partials/07-footer.partial.md +38 -0
  58. package/assets/partials/08-orchestration.partial.md +49 -0
  59. package/assets/partials/09-compact-instructions.partial.md +11 -0
  60. package/assets/partials/_assembly-order.md +40 -0
  61. package/assets/partials-agents/01-header.partial.md +8 -0
  62. package/assets/partials-agents/02-operating-mode.partial.md +44 -0
  63. package/assets/partials-agents/03-orchestration.partial.md +45 -0
  64. package/assets/partials-agents/04-task-protocol.partial.md +69 -0
  65. package/assets/partials-agents/05-coding-constraints.partial.md +20 -0
  66. package/assets/partials-agents/06-quality-safety.partial.md +23 -0
  67. package/assets/partials-agents/07-cloudflare.partial.md +9 -0
  68. package/assets/partials-agents/08-deep-docs.partial.md +32 -0
  69. package/assets/plan-map.json +301 -0
  70. package/assets/plugin-registry.v1.json +8 -0
  71. package/assets/project-structures/ai-native-collaborative-editor.txt +25 -0
  72. package/assets/project-structures/ai-native-product-copilot.txt +21 -0
  73. package/assets/project-structures/ai-native-runtime-console.txt +25 -0
  74. package/assets/project-structures/ai-native-sidecar-kernel.txt +19 -0
  75. package/assets/project-structures/astro-ssr.txt +25 -0
  76. package/assets/project-structures/expo-nativewind.txt +52 -0
  77. package/assets/project-structures/monorepo.txt +63 -0
  78. package/assets/project-structures/remix.txt +50 -0
  79. package/assets/project-structures/tanstack-start-workers.txt +51 -0
  80. package/assets/project-structures/vite-tanstack.txt +58 -0
  81. package/assets/reference-configs/AGENTS.md +13 -0
  82. package/assets/reference-configs/CLAUDE.md +13 -0
  83. package/assets/reference-configs/agentic-development-flow.md +94 -0
  84. package/assets/reference-configs/ai-workflows.md +14 -0
  85. package/assets/reference-configs/changelog-versioning.md +13 -0
  86. package/assets/reference-configs/coding-standards.md +14 -0
  87. package/assets/reference-configs/development-protocol.md +21 -0
  88. package/assets/reference-configs/document-generation.md +38 -0
  89. package/assets/reference-configs/evaluator-rubric.md +19 -0
  90. package/assets/reference-configs/external-tooling.md +374 -0
  91. package/assets/reference-configs/git-strategy.md +14 -0
  92. package/assets/reference-configs/global-working-rules.md +64 -0
  93. package/assets/reference-configs/handoff-protocol.md +38 -0
  94. package/assets/reference-configs/harness-overview.md +124 -0
  95. package/assets/reference-configs/heartbeat-triage.md +57 -0
  96. package/assets/reference-configs/hook-operations.md +96 -0
  97. package/assets/reference-configs/release-deploy.md +40 -0
  98. package/assets/reference-configs/spa-day-protocol.md +14 -0
  99. package/assets/reference-configs/sprint-contracts.md +89 -0
  100. package/assets/reference-configs/workflow-orchestration.md +14 -0
  101. package/assets/skill-hooks.json +41 -0
  102. package/assets/skill-version.json +189 -0
  103. package/assets/skills/claude-review/SKILL.md +231 -0
  104. package/assets/skills/codex-review/SKILL.md +103 -0
  105. package/assets/templates/AGENTS.md +13 -0
  106. package/assets/templates/CLAUDE.md +13 -0
  107. package/assets/templates/architecture.template.md +80 -0
  108. package/assets/templates/brief.template.md +38 -0
  109. package/assets/templates/contract.template.md +104 -0
  110. package/assets/templates/decisions.template.md +85 -0
  111. package/assets/templates/factor-factory/factor-backtest-report.template.md +36 -0
  112. package/assets/templates/factor-factory/factor-hypothesis.template.md +30 -0
  113. package/assets/templates/factor-factory/factor-registry.template.json +5 -0
  114. package/assets/templates/gitignore.template +53 -0
  115. package/assets/templates/guides/metro-esm-gotchas.template.md +46 -0
  116. package/assets/templates/helpers/architecture-event.ts +822 -0
  117. package/assets/templates/helpers/architecture-queue.sh +647 -0
  118. package/assets/templates/helpers/archive-architecture-request.sh +244 -0
  119. package/assets/templates/helpers/archive-workflow.sh +295 -0
  120. package/assets/templates/helpers/capability-config.ts +394 -0
  121. package/assets/templates/helpers/capability-resolver.ts +505 -0
  122. package/assets/templates/helpers/capture-plan.sh +335 -0
  123. package/assets/templates/helpers/check-agent-tooling.sh +1533 -0
  124. package/assets/templates/helpers/check-architecture-sync.sh +287 -0
  125. package/assets/templates/helpers/check-brain-manifest.sh +255 -0
  126. package/assets/templates/helpers/check-context-files.sh +49 -0
  127. package/assets/templates/helpers/check-deploy-sql-order.sh +117 -0
  128. package/assets/templates/helpers/check-skill-version.ts +193 -0
  129. package/assets/templates/helpers/check-task-sync.sh +65 -0
  130. package/assets/templates/helpers/check-task-workflow.sh +1002 -0
  131. package/assets/templates/helpers/codex-handoff-resume.sh +269 -0
  132. package/assets/templates/helpers/context-contract-sync.sh +516 -0
  133. package/assets/templates/helpers/contract-run.ts +465 -0
  134. package/assets/templates/helpers/contract-worktree.sh +725 -0
  135. package/assets/templates/helpers/ensure-task-workflow.sh +1218 -0
  136. package/assets/templates/helpers/harness-trace-grade.sh +150 -0
  137. package/assets/templates/helpers/heartbeat-triage.sh +361 -0
  138. package/assets/templates/helpers/inspect-project-state.ts +280 -0
  139. package/assets/templates/helpers/maintenance-triage.sh +167 -0
  140. package/assets/templates/helpers/migrate-project-template.sh +54 -0
  141. package/assets/templates/helpers/migrate-workflow-docs.ts +413 -0
  142. package/assets/templates/helpers/new-plan.sh +183 -0
  143. package/assets/templates/helpers/new-spec.sh +50 -0
  144. package/assets/templates/helpers/new-sprint.sh +49 -0
  145. package/assets/templates/helpers/plan-to-todo.sh +799 -0
  146. package/assets/templates/helpers/prepare-codex-handoff.sh +203 -0
  147. package/assets/templates/helpers/prepare-handoff.sh +81 -0
  148. package/assets/templates/helpers/refresh-current-status.sh +469 -0
  149. package/assets/templates/helpers/select-agent-context-blocks.sh +66 -0
  150. package/assets/templates/helpers/ship-worktrees.sh +561 -0
  151. package/assets/templates/helpers/sprint-backlog.sh +837 -0
  152. package/assets/templates/helpers/summarize-failures.sh +125 -0
  153. package/assets/templates/helpers/switch-plan.sh +153 -0
  154. package/assets/templates/helpers/sync-brain-docs.sh +381 -0
  155. package/assets/templates/helpers/verify-contract.sh +705 -0
  156. package/assets/templates/helpers/verify-sprint.sh +680 -0
  157. package/assets/templates/helpers/workflow-contract.ts +202 -0
  158. package/assets/templates/helpers/workstream-sync.sh +263 -0
  159. package/assets/templates/implementation-notes.template.md +37 -0
  160. package/assets/templates/packages.template.md +45 -0
  161. package/assets/templates/plan.template.md +77 -0
  162. package/assets/templates/prd.template.md +165 -0
  163. package/assets/templates/research.template.md +25 -0
  164. package/assets/templates/review.template.md +78 -0
  165. package/assets/templates/spec.template.md +31 -0
  166. package/assets/templates/sprint.template.md +69 -0
  167. package/assets/templates/tech-stack.template.md +57 -0
  168. package/assets/versions.json +77 -0
  169. package/assets/workflow-contract.v1.json +671 -0
  170. package/bin/forge-desktop-helper.mjs +204 -0
  171. package/bin/forge-hook.mjs +32 -0
  172. package/bin/forge-runtime-service.mjs +34 -0
  173. package/bin/forge-runtime.mjs +30 -0
  174. package/bin/forge.mjs +32 -0
  175. package/docs/CHANGELOG.md +978 -0
  176. package/docs/README.md +72 -0
  177. package/docs/README.zh-CN.md +58 -0
  178. package/docs/architecture/chatgpt-handoff-facade.md +393 -0
  179. package/docs/architecture/current/README.md +158 -0
  180. package/docs/architecture/current/approved-target-architecture.zh-CN.md +1501 -0
  181. package/docs/architecture/current/architecture-invariants.md +331 -0
  182. package/docs/architecture/current/automation-and-schedule-engine.md +453 -0
  183. package/docs/architecture/current/codegraph-context-provider.md +226 -0
  184. package/docs/architecture/current/control-plane-authority-inventory.md +82 -0
  185. package/docs/architecture/current/controller-repository-decoupling.md +148 -0
  186. package/docs/architecture/current/dispatch-and-agent-strategy.md +354 -0
  187. package/docs/architecture/current/entity-model.md +619 -0
  188. package/docs/architecture/current/external-plugin-governance.md +396 -0
  189. package/docs/architecture/current/failure-recovery.md +471 -0
  190. package/docs/architecture/current/governance.md +213 -0
  191. package/docs/architecture/current/human-interaction-plane.md +87 -0
  192. package/docs/architecture/current/implementation-status.md +128 -0
  193. package/docs/architecture/current/job-and-run-lifecycle.md +413 -0
  194. package/docs/architecture/current/local-workspace-target-grants.md +70 -0
  195. package/docs/architecture/current/migration-roadmap.md +105 -0
  196. package/docs/architecture/current/multi-repository-execution.md +371 -0
  197. package/docs/architecture/current/operations-runbook.md +45 -0
  198. package/docs/architecture/current/peer-supercontroller-runtime.md +32 -0
  199. package/docs/architecture/current/personal-assistant-plugin-baseline.md +207 -0
  200. package/docs/architecture/current/repository-access-modes.md +102 -0
  201. package/docs/architecture/current/route-policy.md +15 -0
  202. package/docs/architecture/current/route-session-concurrency.md +85 -0
  203. package/docs/architecture/current/runtime-architecture-simplification.md +214 -0
  204. package/docs/architecture/current/runtime-directory-map.md +49 -0
  205. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +85 -0
  206. package/docs/architecture/current/scheduler-and-resource-claims.md +432 -0
  207. package/docs/architecture/current/session-aware-execution-and-authorization.md +77 -0
  208. package/docs/architecture/current/system-overview.md +144 -0
  209. package/docs/architecture/current/target-requirements-traceability.md +202 -0
  210. package/docs/architecture/current/thin-harness-governance-policy.md +32 -0
  211. package/docs/architecture/current/thin-harness-v1.md +313 -0
  212. package/docs/architecture/current/verification-and-release-gates.md +391 -0
  213. package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +100 -0
  214. package/docs/architecture/decisions/20260803-canonical-skill-surface.md +40 -0
  215. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +53 -0
  216. package/docs/architecture/diagrams/.gitkeep +1 -0
  217. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +186 -0
  218. package/docs/architecture/domains/.gitkeep +0 -0
  219. package/docs/architecture/domains/public-surface.md +30 -0
  220. package/docs/architecture/domains/runtime-harness.md +33 -0
  221. package/docs/architecture/domains/verification.md +33 -0
  222. package/docs/architecture/domains/workflow-engine.md +30 -0
  223. package/docs/architecture/global-hook-runtime.md +162 -0
  224. package/docs/architecture/history.md +38 -0
  225. package/docs/architecture/index.md +114 -0
  226. package/docs/architecture/ios-semantic-automation-provider-v2.md +166 -0
  227. package/docs/architecture/modules/.gitkeep +0 -0
  228. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +24 -0
  229. package/docs/architecture/requests/.gitkeep +1 -0
  230. package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
  231. package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +59 -0
  232. package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +59 -0
  233. package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +59 -0
  234. package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +59 -0
  235. package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
  236. package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
  237. package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
  238. package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
  239. package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +59 -0
  240. package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
  241. package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
  242. package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
  243. package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
  244. package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
  245. package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
  246. package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
  247. package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
  248. package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
  249. package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +59 -0
  250. package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +59 -0
  251. package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +63 -0
  252. package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
  253. package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
  254. package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
  255. package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
  256. package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +60 -0
  257. package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +60 -0
  258. package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +68 -0
  259. package/docs/architecture/requests/archive/2026/root.md +68 -0
  260. package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +76 -0
  261. package/docs/architecture/requests/archive/2026/verification-evals-checks.md +68 -0
  262. package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +69 -0
  263. package/docs/architecture/reviews/2026-07-11-access-surface-review.md +99 -0
  264. package/docs/architecture/snapshots/.gitkeep +1 -0
  265. package/docs/forge-autonomous-goal-loop.md +7 -0
  266. package/docs/forge-chatgpt-browser-engine.md +11 -0
  267. package/docs/forge-chatgpt-mcp-setup.md +239 -0
  268. package/docs/forge-chatgpt-supervised-automation.md +11 -0
  269. package/docs/forge-github-issue-launcher.md +7 -0
  270. package/docs/forge-plugin-management.md +95 -0
  271. package/docs/forge-provider-configuration.md +7 -0
  272. package/docs/forge-runtime-self-healing-loop.md +7 -0
  273. package/docs/images/forge-banner-cn.svg +1 -0
  274. package/docs/images/forge-banner.svg +1 -0
  275. package/docs/images/forge-controller-flow.svg +1 -0
  276. package/docs/images/image.png +0 -0
  277. package/docs/images/repo-harness-gptpro-cn.png +0 -0
  278. package/docs/images/repo-harness-gptpro.png +0 -0
  279. package/docs/images/repo-harness-hook-carrot.png +0 -0
  280. package/docs/operations/chatgpt-personal-assistant-runtime.md +83 -0
  281. package/docs/operations/checkout-route-integrity.md +22 -0
  282. package/docs/operations/controller-browser-plugin.md +299 -0
  283. package/docs/operations/controller-desktop-plugin.md +112 -0
  284. package/docs/operations/controller-performance-and-502.md +126 -0
  285. package/docs/operations/controller-reliability-runbook.md +134 -0
  286. package/docs/operations/environment-hardening.md +50 -0
  287. package/docs/operations/features.md +45 -0
  288. package/docs/operations/features.zh-CN.md +45 -0
  289. package/docs/operations/github-repository.md +60 -0
  290. package/docs/operations/gmail-assistant-routines.md +186 -0
  291. package/docs/operations/homebrew.md +26 -0
  292. package/docs/operations/local-system-assistant.md +46 -0
  293. package/docs/operations/local-system-assistant.zh-CN.md +46 -0
  294. package/docs/operations/mcp-tool-exposure.md +58 -0
  295. package/docs/operations/mobile-intents-shortcuts.md +117 -0
  296. package/docs/operations/open-source-release-hygiene.md +31 -0
  297. package/docs/operations/personal-assistant-operator-guide.md +61 -0
  298. package/docs/operations/platform-support.md +62 -0
  299. package/docs/operations/platform-support.zh-CN.md +61 -0
  300. package/docs/operations/provider-configuration.md +31 -0
  301. package/docs/operations/releasing.md +86 -0
  302. package/docs/operations/releasing.zh-CN.md +86 -0
  303. package/docs/operations/route-session-concurrency-baseline.json +893 -0
  304. package/docs/operations/route-session-concurrency-baseline.md +94 -0
  305. package/docs/operations/runtime-performance-diagnostics.md +14 -0
  306. package/docs/operations/sqlite-control-plane-recovery.md +65 -0
  307. package/docs/operations/standalone-disaster-recovery.md +136 -0
  308. package/docs/operations/troubleshooting.md +72 -0
  309. package/docs/operations/troubleshooting.zh-CN.md +70 -0
  310. package/docs/public-usage-guide.md +33 -0
  311. package/docs/public-usage-guide.zh-CN.md +33 -0
  312. package/docs/tutorials/01-install-and-start.md +93 -0
  313. package/docs/tutorials/01-install-and-start.zh-CN.md +93 -0
  314. package/docs/tutorials/02-connect-chatgpt.md +82 -0
  315. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +80 -0
  316. package/docs/tutorials/03-first-repository-task.md +57 -0
  317. package/docs/tutorials/03-first-repository-task.zh-CN.md +57 -0
  318. package/docs/tutorials/README.md +13 -0
  319. package/docs/tutorials/README.zh-CN.md +13 -0
  320. package/docs/wiki/Architecture.md +85 -0
  321. package/docs/wiki/Core-Concepts.md +27 -0
  322. package/docs/wiki/Home.md +36 -0
  323. package/docs/wiki/Implementation.md +172 -0
  324. package/docs/wiki/Installation.md +50 -0
  325. package/docs/wiki/Integrations.md +29 -0
  326. package/docs/wiki/Operations.md +26 -0
  327. package/docs/wiki/Quick-Start.md +46 -0
  328. package/docs/wiki/Releases-and-Upgrades.md +34 -0
  329. package/docs/wiki/Runtime-Architecture.md +50 -0
  330. package/docs/wiki/Security-Model.md +31 -0
  331. package/docs/wiki/Troubleshooting.md +38 -0
  332. package/docs/wiki/Work-Lifecycle.md +31 -0
  333. package/docs/wiki/_Sidebar.md +21 -0
  334. package/install.ps1 +48 -0
  335. package/install.sh +66 -0
  336. package/package.json +156 -0
  337. package/scripts/activate-source-baseline.ts +335 -0
  338. package/scripts/architecture-event.ts +822 -0
  339. package/scripts/architecture-queue.sh +647 -0
  340. package/scripts/archive-architecture-request.sh +244 -0
  341. package/scripts/archive-workflow.sh +295 -0
  342. package/scripts/assemble-template.ts +1172 -0
  343. package/scripts/benchmark-controller-hotpath.ts +212 -0
  344. package/scripts/benchmark-controller-round2.ts +139 -0
  345. package/scripts/benchmark-ios-agent-device.ts +514 -0
  346. package/scripts/benchmark-route-session-concurrency.ts +714 -0
  347. package/scripts/benchmark-thin-harness-gateway-ab.ts +632 -0
  348. package/scripts/benchmark-thin-harness-mcp-e2e.ts +479 -0
  349. package/scripts/benchmark-thin-harness.ts +251 -0
  350. package/scripts/bootstrap-runtime-maintenance-recovery.sh +183 -0
  351. package/scripts/canary-global-hook.sh +263 -0
  352. package/scripts/capability-config.ts +394 -0
  353. package/scripts/capability-resolver.ts +505 -0
  354. package/scripts/capture-plan.sh +335 -0
  355. package/scripts/check-agent-tooling.sh +1549 -0
  356. package/scripts/check-architecture-sync.sh +373 -0
  357. package/scripts/check-brain-manifest.sh +255 -0
  358. package/scripts/check-ci.sh +16 -0
  359. package/scripts/check-context-files.sh +49 -0
  360. package/scripts/check-deploy-sql-order.sh +117 -0
  361. package/scripts/check-mcp-compatibility.ts +93 -0
  362. package/scripts/check-npm-release.sh +32 -0
  363. package/scripts/check-open-source-tracked-surface.sh +146 -0
  364. package/scripts/check-package-identity.mjs +117 -0
  365. package/scripts/check-platform-support.mjs +79 -0
  366. package/scripts/check-public-docs.sh +105 -0
  367. package/scripts/check-public-release-surface.sh +19 -0
  368. package/scripts/check-release-published.sh +57 -0
  369. package/scripts/check-release-readiness.sh +43 -0
  370. package/scripts/check-release-version.mjs +74 -0
  371. package/scripts/check-runtime-architecture.mjs +593 -0
  372. package/scripts/check-runtime-env.mjs +89 -0
  373. package/scripts/check-skill-version.ts +193 -0
  374. package/scripts/check-tarball-install-smoke.sh +66 -0
  375. package/scripts/check-task-sync.sh +65 -0
  376. package/scripts/check-task-workflow.sh +1002 -0
  377. package/scripts/check-third-party-notices.mjs +36 -0
  378. package/scripts/codex-handoff-resume.sh +269 -0
  379. package/scripts/complete-control-plane-cutover.ts +126 -0
  380. package/scripts/context-contract-sync.sh +516 -0
  381. package/scripts/contract-run.ts +465 -0
  382. package/scripts/contract-worktree.sh +725 -0
  383. package/scripts/controller-ngrok-rotation.sh +678 -0
  384. package/scripts/create-project-dirs.sh +282 -0
  385. package/scripts/ensure-codegraph.sh +16 -0
  386. package/scripts/ensure-task-workflow.sh +1218 -0
  387. package/scripts/export-requirement-portfolio.ts +28 -0
  388. package/scripts/factor-lab-check.sh +73 -0
  389. package/scripts/factor-lab-new.sh +96 -0
  390. package/scripts/factor-lab-promote.sh +86 -0
  391. package/scripts/factor-lab-reject.sh +68 -0
  392. package/scripts/harness-trace-grade.sh +150 -0
  393. package/scripts/heartbeat-triage.sh +361 -0
  394. package/scripts/hook-dispatch-diet-report.ts +234 -0
  395. package/scripts/hook-shim.sh +93 -0
  396. package/scripts/init-project.sh +505 -0
  397. package/scripts/initializer-question-pack.ts +200 -0
  398. package/scripts/inspect-project-state.ts +280 -0
  399. package/scripts/install-desktop-operator-registration.ts +45 -0
  400. package/scripts/install-standalone-recovery.ts +74 -0
  401. package/scripts/lib/AGENTS.md +13 -0
  402. package/scripts/lib/CLAUDE.md +13 -0
  403. package/scripts/lib/controller-home-env.sh +69 -0
  404. package/scripts/lib/project-init-lib.sh +2759 -0
  405. package/scripts/load-standalone-recovery.sh +17 -0
  406. package/scripts/loop-engine-cutover-gate.ts +324 -0
  407. package/scripts/maintenance-triage.sh +167 -0
  408. package/scripts/migrate-project-template.sh +1189 -0
  409. package/scripts/migrate-requirement-portfolio.ts +86 -0
  410. package/scripts/migrate-workflow-docs.ts +413 -0
  411. package/scripts/new-plan.sh +183 -0
  412. package/scripts/new-spec.sh +50 -0
  413. package/scripts/new-sprint.sh +49 -0
  414. package/scripts/open-source-audit-allowlist.txt +46 -0
  415. package/scripts/package-source-archive.sh +147 -0
  416. package/scripts/plan-to-todo.sh +799 -0
  417. package/scripts/prepare-codex-handoff.sh +203 -0
  418. package/scripts/prepare-handoff.sh +81 -0
  419. package/scripts/public-release-files.txt +39 -0
  420. package/scripts/public-release.ts +174 -0
  421. package/scripts/publish-release-tarball.sh +27 -0
  422. package/scripts/refresh-current-status.sh +469 -0
  423. package/scripts/refresh-ios-physical-runtime.ts +196 -0
  424. package/scripts/repair-executable-modes.ts +8 -0
  425. package/scripts/rollout-all-registered-repos.sh +7 -0
  426. package/scripts/route-nl-vs-ts-eval.ts +679 -0
  427. package/scripts/run-bun-test-file.ts +182 -0
  428. package/scripts/run-governed-gate.ts +140 -0
  429. package/scripts/run-mobile-monitor-bridge.ts +71 -0
  430. package/scripts/run-skill-evals.ts +1022 -0
  431. package/scripts/run-skill-hook.ts +244 -0
  432. package/scripts/run-supervised-command.ts +24 -0
  433. package/scripts/run-tests-portable.sh +20 -0
  434. package/scripts/select-agent-context-blocks.sh +66 -0
  435. package/scripts/setup-plugins.sh +43 -0
  436. package/scripts/ship-worktrees.sh +561 -0
  437. package/scripts/smoke-browser-cdp-bridge.ts +93 -0
  438. package/scripts/smoke-chatgpt-supervised-automation.ts +208 -0
  439. package/scripts/smoke-ios-physical-device.ts +114 -0
  440. package/scripts/smoke-mcp-http-runtime.ts +216 -0
  441. package/scripts/smoke-mcp-tool-surface.ts +117 -0
  442. package/scripts/smoke-runtime-recovery.ts +220 -0
  443. package/scripts/smoke-schedule-engine.ts +283 -0
  444. package/scripts/sprint-backlog.sh +837 -0
  445. package/scripts/stage-runtime-release.ts +38 -0
  446. package/scripts/summarize-failures.sh +125 -0
  447. package/scripts/switch-plan.sh +153 -0
  448. package/scripts/sync-brain-docs.sh +381 -0
  449. package/scripts/sync-codex-installed-copies.sh +183 -0
  450. package/scripts/test-governance.ts +75 -0
  451. package/scripts/verify-contract.sh +705 -0
  452. package/scripts/verify-forge-runtime.sh +31 -0
  453. package/scripts/verify-sprint.sh +680 -0
  454. package/scripts/windows-paths.d.mts +7 -0
  455. package/scripts/windows-paths.mjs +28 -0
  456. package/scripts/workflow-contract.ts +206 -0
  457. package/scripts/workstream-sync.sh +263 -0
  458. package/src/cli/agent-jobs/executable-resolver.ts +289 -0
  459. package/src/cli/agent-jobs/executor-health.ts +252 -0
  460. package/src/cli/agent-jobs/integration.ts +963 -0
  461. package/src/cli/agent-jobs/job-manager.ts +2030 -0
  462. package/src/cli/agent-jobs/types.ts +219 -0
  463. package/src/cli/agent-jobs/worker-lifecycle.ts +288 -0
  464. package/src/cli/chatgpt-browser/bind-server.ts +504 -0
  465. package/src/cli/chatgpt-browser/binding.ts +140 -0
  466. package/src/cli/chatgpt-browser/bridge-extension.ts +221 -0
  467. package/src/cli/chatgpt-browser/bridge-provider.ts +268 -0
  468. package/src/cli/chatgpt-browser/engine.ts +569 -0
  469. package/src/cli/chatgpt-browser/file-policy.ts +158 -0
  470. package/src/cli/chatgpt-browser/native-provider.ts +553 -0
  471. package/src/cli/chatgpt-browser/oracle-provider.ts +437 -0
  472. package/src/cli/chatgpt-browser/prompt-assembler.ts +29 -0
  473. package/src/cli/chatgpt-browser/session-store.ts +301 -0
  474. package/src/cli/chatgpt-browser/types.ts +172 -0
  475. package/src/cli/commands/adopt-plan.ts +186 -0
  476. package/src/cli/commands/brain-root.ts +132 -0
  477. package/src/cli/commands/brain.ts +609 -0
  478. package/src/cli/commands/capability-context.ts +613 -0
  479. package/src/cli/commands/chatgpt.ts +424 -0
  480. package/src/cli/commands/controller.ts +680 -0
  481. package/src/cli/commands/docs.ts +103 -0
  482. package/src/cli/commands/doctor.ts +476 -0
  483. package/src/cli/commands/global-runtime.ts +323 -0
  484. package/src/cli/commands/hook.ts +34 -0
  485. package/src/cli/commands/init-hook.ts +870 -0
  486. package/src/cli/commands/init.ts +829 -0
  487. package/src/cli/commands/install.ts +86 -0
  488. package/src/cli/commands/mcp.ts +315 -0
  489. package/src/cli/commands/migrate.ts +154 -0
  490. package/src/cli/commands/plugin.ts +74 -0
  491. package/src/cli/commands/prompt-guard-decision.ts +238 -0
  492. package/src/cli/commands/recovery.ts +667 -0
  493. package/src/cli/commands/repository.ts +224 -0
  494. package/src/cli/commands/run.ts +23 -0
  495. package/src/cli/commands/runtime.ts +191 -0
  496. package/src/cli/commands/security.ts +440 -0
  497. package/src/cli/commands/status.ts +137 -0
  498. package/src/cli/commands/tools.ts +167 -0
  499. package/src/cli/controller/check-runner.ts +1022 -0
  500. package/src/cli/controller/codex-continuation.ts +153 -0
  501. package/src/cli/controller/completion-backlog.ts +380 -0
  502. package/src/cli/controller/completion-orchestrator.ts +1807 -0
  503. package/src/cli/controller/completion-target.ts +96 -0
  504. package/src/cli/controller/composite-operations.ts +196 -0
  505. package/src/cli/controller/composite-result.ts +130 -0
  506. package/src/cli/controller/context-pack.ts +652 -0
  507. package/src/cli/controller/execution-completion.ts +175 -0
  508. package/src/cli/controller/execution-policy.ts +392 -0
  509. package/src/cli/controller/governance.ts +594 -0
  510. package/src/cli/controller/issue-store.ts +1421 -0
  511. package/src/cli/controller/launch-agents.ts +410 -0
  512. package/src/cli/controller/legacy-issue-cutover.ts +415 -0
  513. package/src/cli/controller/lifecycle-authority.ts +16 -0
  514. package/src/cli/controller/migrated-issue-decision.ts +237 -0
  515. package/src/cli/controller/operational-plan.ts +226 -0
  516. package/src/cli/controller/postcondition.ts +134 -0
  517. package/src/cli/controller/progress.ts +656 -0
  518. package/src/cli/controller/project-state.ts +147 -0
  519. package/src/cli/controller/requirement-portfolio-export.ts +146 -0
  520. package/src/cli/controller/requirement-portfolio-migration.ts +613 -0
  521. package/src/cli/controller/run-evidence.ts +43 -0
  522. package/src/cli/controller/runtime-config.ts +92 -0
  523. package/src/cli/controller/stuck-state-migration.ts +396 -0
  524. package/src/cli/controller/task-ledger.ts +632 -0
  525. package/src/cli/controller/task-status-resolver.ts +403 -0
  526. package/src/cli/controller/types.ts +273 -0
  527. package/src/cli/controller/work-mode.ts +186 -0
  528. package/src/cli/controller/worklog.ts +201 -0
  529. package/src/cli/diagnostic-entry.ts +39 -0
  530. package/src/cli/editing/edit-session.ts +1606 -0
  531. package/src/cli/editing/executable-modes.ts +70 -0
  532. package/src/cli/github/github.ts +274 -0
  533. package/src/cli/github/plugin.ts +345 -0
  534. package/src/cli/github/session.ts +104 -0
  535. package/src/cli/hook/prompt-guard-decision.ts +238 -0
  536. package/src/cli/hook/prompt-intents.ts +534 -0
  537. package/src/cli/hook/route-registry.ts +107 -0
  538. package/src/cli/hook/runtime.ts +303 -0
  539. package/src/cli/hook/state-snapshot.ts +388 -0
  540. package/src/cli/hook-entry.ts +51 -0
  541. package/src/cli/index.ts +574 -0
  542. package/src/cli/installer/managed-entries.ts +78 -0
  543. package/src/cli/installer/shared.ts +65 -0
  544. package/src/cli/installer/targets/claude.ts +136 -0
  545. package/src/cli/installer/targets/codex.ts +174 -0
  546. package/src/cli/installer/targets/registry.ts +28 -0
  547. package/src/cli/installer/types.ts +72 -0
  548. package/src/cli/local-bridge/connector-freshness.ts +529 -0
  549. package/src/cli/local-bridge/console-view-models.ts +324 -0
  550. package/src/cli/local-bridge/dashboard.ts +1312 -0
  551. package/src/cli/local-bridge/facade-api.ts +1877 -0
  552. package/src/cli/local-bridge/job-store.ts +1451 -0
  553. package/src/cli/local-bridge/mobile-intents.ts +344 -0
  554. package/src/cli/local-bridge/mobile-monitor.ts +209 -0
  555. package/src/cli/local-bridge/server.ts +3183 -0
  556. package/src/cli/local-bridge/types.ts +176 -0
  557. package/src/cli/mcp/access-mode.ts +132 -0
  558. package/src/cli/mcp/access-tools.ts +387 -0
  559. package/src/cli/mcp/audit.ts +32 -0
  560. package/src/cli/mcp/auth.ts +402 -0
  561. package/src/cli/mcp/instructions.ts +33 -0
  562. package/src/cli/mcp/legacy-context.ts +16 -0
  563. package/src/cli/mcp/legacy-tool-service.ts +5516 -0
  564. package/src/cli/mcp/multi-repository.ts +423 -0
  565. package/src/cli/mcp/oauth.ts +226 -0
  566. package/src/cli/mcp/paths.ts +134 -0
  567. package/src/cli/mcp/policy.ts +230 -0
  568. package/src/cli/mcp/proxy-env.ts +62 -0
  569. package/src/cli/mcp/readiness-projection.ts +24 -0
  570. package/src/cli/mcp/redaction.ts +21 -0
  571. package/src/cli/mcp/repo.ts +24 -0
  572. package/src/cli/mcp/repository-tools.ts +1295 -0
  573. package/src/cli/mcp/server.ts +341 -0
  574. package/src/cli/mcp/setup.ts +793 -0
  575. package/src/cli/mcp/tool-capability-cache.ts +87 -0
  576. package/src/cli/mcp/tools.ts +9 -0
  577. package/src/cli/mcp/toolset-names.ts +70 -0
  578. package/src/cli/mcp/toolset-selection.ts +41 -0
  579. package/src/cli/mcp/toolset.ts +300 -0
  580. package/src/cli/mcp/transports/AGENTS.md +39 -0
  581. package/src/cli/mcp/transports/CLAUDE.md +39 -0
  582. package/src/cli/mcp/transports/http.ts +1234 -0
  583. package/src/cli/mcp/transports/session-registry.ts +428 -0
  584. package/src/cli/mcp/transports/stdio.ts +8 -0
  585. package/src/cli/mcp/types.ts +41 -0
  586. package/src/cli/mcp/umbrella-tools.ts +117 -0
  587. package/src/cli/repo-adoption/reclaim-runtime.ts +681 -0
  588. package/src/cli/repositories/branch-name-policy.ts +57 -0
  589. package/src/cli/repositories/command-classifier.ts +600 -0
  590. package/src/cli/repositories/command-executor.ts +1066 -0
  591. package/src/cli/repositories/command-normalization.ts +53 -0
  592. package/src/cli/repositories/command-scope.ts +264 -0
  593. package/src/cli/repositories/controller-home.ts +88 -0
  594. package/src/cli/repositories/entity-migration.ts +199 -0
  595. package/src/cli/repositories/ephemeral-workspace.ts +62 -0
  596. package/src/cli/repositories/git-command-executor.ts +301 -0
  597. package/src/cli/repositories/goal-registry.ts +239 -0
  598. package/src/cli/repositories/identity.ts +64 -0
  599. package/src/cli/repositories/local-project-onboarding.ts +539 -0
  600. package/src/cli/repositories/locks.ts +262 -0
  601. package/src/cli/repositories/registry.ts +1057 -0
  602. package/src/cli/repositories/runtime-storage.ts +411 -0
  603. package/src/cli/repositories/safe-patch.ts +421 -0
  604. package/src/cli/repositories/selected-path-actions.ts +341 -0
  605. package/src/cli/repositories/structured-git.ts +257 -0
  606. package/src/cli/repositories/types.ts +155 -0
  607. package/src/cli/repositories/umbrella.ts +134 -0
  608. package/src/cli/repositories/workbench.ts +140 -0
  609. package/src/cli/repositories/worktree-storage.ts +137 -0
  610. package/src/cli/repository/inspector.ts +637 -0
  611. package/src/cli/repository/session-cache.ts +453 -0
  612. package/src/cli/runtime/helper-runner.ts +172 -0
  613. package/src/cli/runtime-invocation.ts +318 -0
  614. package/src/cli/tools/codegraph.ts +676 -0
  615. package/src/cli/v81-entry.ts +29 -0
  616. package/src/core/adoption/gitignore-plan.ts +127 -0
  617. package/src/core/adoption/helper-wrapper-plan.ts +159 -0
  618. package/src/core/adoption/manifest-templates.ts +60 -0
  619. package/src/core/adoption/modes.ts +7 -0
  620. package/src/core/adoption/operations.ts +126 -0
  621. package/src/core/adoption/plan.ts +149 -0
  622. package/src/core/adoption/render.ts +70 -0
  623. package/src/core/adoption/rollback.ts +62 -0
  624. package/src/core/adoption/summary.ts +43 -0
  625. package/src/core/adoption/workflow-contract-asset.ts +14 -0
  626. package/src/core/adoption/workflow-contract-plan.ts +31 -0
  627. package/src/effects/fs-transaction.ts +540 -0
  628. package/src/effects/git-executable.ts +59 -0
  629. package/src/effects/managed-block.ts +86 -0
  630. package/src/effects/path-safety.ts +47 -0
  631. package/src/effects/process-runner.ts +192 -0
  632. package/src/runtime/assistant/action-proposals.ts +279 -0
  633. package/src/runtime/assistant/intent.ts +395 -0
  634. package/src/runtime/assistant/model-provider.ts +460 -0
  635. package/src/runtime/assistant/openapi.ts +223 -0
  636. package/src/runtime/assistant/policy.ts +89 -0
  637. package/src/runtime/assistant/readiness.ts +258 -0
  638. package/src/runtime/assistant/routine-runtime.ts +477 -0
  639. package/src/runtime/assistant/schedule-binding.ts +171 -0
  640. package/src/runtime/assistant/standing-grants.ts +375 -0
  641. package/src/runtime/assistant/store.ts +243 -0
  642. package/src/runtime/assistant/types.ts +135 -0
  643. package/src/runtime/context/codegraph-read-provider.ts +347 -0
  644. package/src/runtime/context/codegraph-sidecar.cjs +207 -0
  645. package/src/runtime/control-plane/dispatch-priority.ts +67 -0
  646. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +151 -0
  647. package/src/runtime/control-plane/execution/execution-identity.ts +456 -0
  648. package/src/runtime/control-plane/execution/session-store.ts +175 -0
  649. package/src/runtime/control-plane/execution/validation.ts +117 -0
  650. package/src/runtime/control-plane/execution/verification-evidence.ts +191 -0
  651. package/src/runtime/control-plane/execution/work-handle-store.ts +225 -0
  652. package/src/runtime/control-plane/execution/work-prepare-request-store.ts +159 -0
  653. package/src/runtime/control-plane/execution/work-task-receipt.ts +445 -0
  654. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +821 -0
  655. package/src/runtime/control-plane/facade/capability-registry.ts +279 -0
  656. package/src/runtime/control-plane/facade/check-normalization.ts +233 -0
  657. package/src/runtime/control-plane/facade/codex-delegation.ts +196 -0
  658. package/src/runtime/control-plane/facade/controller-session-store.ts +199 -0
  659. package/src/runtime/control-plane/facade/facade-result.ts +41 -0
  660. package/src/runtime/control-plane/facade/goal-workloop-access.ts +177 -0
  661. package/src/runtime/control-plane/facade/goal-workloop.ts +1216 -0
  662. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +221 -0
  663. package/src/runtime/control-plane/facade/index.ts +16 -0
  664. package/src/runtime/control-plane/facade/operation-digest.ts +528 -0
  665. package/src/runtime/control-plane/facade/plan-contract-store.ts +403 -0
  666. package/src/runtime/control-plane/facade/policy-gate.ts +149 -0
  667. package/src/runtime/control-plane/facade/requirement-board.ts +280 -0
  668. package/src/runtime/control-plane/facade/self-healing-loop.ts +555 -0
  669. package/src/runtime/control-plane/facade/suggested-actions.ts +108 -0
  670. package/src/runtime/control-plane/facade/types.ts +729 -0
  671. package/src/runtime/control-plane/facade/work-admission-policy.ts +93 -0
  672. package/src/runtime/control-plane/facade/work-continuation.ts +104 -0
  673. package/src/runtime/control-plane/facade/work-contract-store.ts +1118 -0
  674. package/src/runtime/control-plane/facade/work-task-projection.ts +66 -0
  675. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +365 -0
  676. package/src/runtime/control-plane/global-scheduler/scheduler.ts +920 -0
  677. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +89 -0
  678. package/src/runtime/control-plane/goal-loop/config-facade.ts +700 -0
  679. package/src/runtime/control-plane/goal-loop/config-store.ts +570 -0
  680. package/src/runtime/control-plane/goal-loop/config-types.ts +213 -0
  681. package/src/runtime/control-plane/goal-loop/executor-router.ts +109 -0
  682. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +149 -0
  683. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +304 -0
  684. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +1001 -0
  685. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +160 -0
  686. package/src/runtime/control-plane/goal-loop/index.ts +13 -0
  687. package/src/runtime/control-plane/goal-loop/local-tools.ts +290 -0
  688. package/src/runtime/control-plane/goal-loop/policy-gates.ts +203 -0
  689. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +93 -0
  690. package/src/runtime/control-plane/goal-loop/provider-registry.ts +545 -0
  691. package/src/runtime/control-plane/goal-loop/types.ts +378 -0
  692. package/src/runtime/control-plane/governance/access-policy.ts +147 -0
  693. package/src/runtime/control-plane/governance/authorization.ts +195 -0
  694. package/src/runtime/control-plane/governance/external-effects.ts +43 -0
  695. package/src/runtime/control-plane/launcher/thin-launcher.ts +97 -0
  696. package/src/runtime/control-plane/persistence/metadata-payload-policy.ts +53 -0
  697. package/src/runtime/control-plane/persistence/requirement-store.ts +241 -0
  698. package/src/runtime/control-plane/persistence/sqlite-store.ts +539 -0
  699. package/src/runtime/control-plane/release-retention.ts +447 -0
  700. package/src/runtime/control-plane/repo-actor/actor.ts +181 -0
  701. package/src/runtime/control-plane/repo-actor/registry.ts +20 -0
  702. package/src/runtime/control-plane/routing/index.ts +1 -0
  703. package/src/runtime/control-plane/routing/route-policy.ts +379 -0
  704. package/src/runtime/control-plane/runtime-cleanup.ts +727 -0
  705. package/src/runtime/control-plane/runtime-generation.ts +509 -0
  706. package/src/runtime/control-plane/runtime-pid-ownership.ts +11 -0
  707. package/src/runtime/control-plane/runtime-status-client.ts +90 -0
  708. package/src/runtime/control-plane/startup-recovery.ts +198 -0
  709. package/src/runtime/diagnostics/connectivity-observability.ts +475 -0
  710. package/src/runtime/diagnostics/mcp-timing.ts +64 -0
  711. package/src/runtime/diagnostics/performance.ts +379 -0
  712. package/src/runtime/diagnostics/process-facade.ts +350 -0
  713. package/src/runtime/diagnostics/read-only-tool.ts +226 -0
  714. package/src/runtime/evidence/artifact-store.ts +154 -0
  715. package/src/runtime/evidence/event-ledger.ts +80 -0
  716. package/src/runtime/evidence/evidence-store.ts +68 -0
  717. package/src/runtime/evidence/process-check-receipt.ts +43 -0
  718. package/src/runtime/evidence/result-store.ts +209 -0
  719. package/src/runtime/evidence/sensitive-output.ts +353 -0
  720. package/src/runtime/execution/jobs/child-reference.ts +124 -0
  721. package/src/runtime/execution/jobs/legacy-adapter.ts +22 -0
  722. package/src/runtime/execution/jobs/receipt-recovery.ts +119 -0
  723. package/src/runtime/execution/jobs/receipt-store.ts +243 -0
  724. package/src/runtime/execution/jobs/store.ts +737 -0
  725. package/src/runtime/execution/jobs/timeouts.ts +109 -0
  726. package/src/runtime/execution/jobs/types.ts +230 -0
  727. package/src/runtime/execution/jobs/wait.ts +45 -0
  728. package/src/runtime/execution/process-runtime/canary.ts +26 -0
  729. package/src/runtime/execution/process-runtime/check-facade.ts +272 -0
  730. package/src/runtime/execution/process-runtime/check-receipt.ts +185 -0
  731. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +60 -0
  732. package/src/runtime/execution/process-runtime/command-facade.ts +321 -0
  733. package/src/runtime/execution/process-runtime/gc.ts +171 -0
  734. package/src/runtime/execution/process-runtime/index.ts +9 -0
  735. package/src/runtime/execution/process-runtime/interactive-admission.ts +23 -0
  736. package/src/runtime/execution/process-runtime/process-runner-entry.ts +466 -0
  737. package/src/runtime/execution/process-runtime/resource-claims.ts +440 -0
  738. package/src/runtime/execution/process-runtime/runtime.ts +1816 -0
  739. package/src/runtime/execution/process-runtime/store.ts +687 -0
  740. package/src/runtime/execution/process-runtime/types.ts +352 -0
  741. package/src/runtime/execution/thin-harness/async-process.ts +181 -0
  742. package/src/runtime/execution/thin-harness/batch-executor.ts +671 -0
  743. package/src/runtime/execution/thin-harness/execution-router.ts +700 -0
  744. package/src/runtime/execution/thin-harness/fast-executor.ts +1263 -0
  745. package/src/runtime/execution/thin-harness/fast-receipt.ts +235 -0
  746. package/src/runtime/execution/thin-harness/fingerprint-worker.ts +191 -0
  747. package/src/runtime/execution/thin-harness/index.ts +13 -0
  748. package/src/runtime/execution/thin-harness/latency-trace.ts +107 -0
  749. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +745 -0
  750. package/src/runtime/execution/thin-harness/mutation-gate.ts +578 -0
  751. package/src/runtime/execution/thin-harness/proposal-store.ts +409 -0
  752. package/src/runtime/execution/thin-harness/request-ledger.ts +595 -0
  753. package/src/runtime/execution/thin-harness/search-worker.ts +114 -0
  754. package/src/runtime/execution/thin-harness/types.ts +313 -0
  755. package/src/runtime/execution/thin-harness/workspace-savepoint.ts +333 -0
  756. package/src/runtime/execution/workers/executor.ts +687 -0
  757. package/src/runtime/execution/workers/ownership.ts +94 -0
  758. package/src/runtime/execution/workers/worker-entry.ts +277 -0
  759. package/src/runtime/gateway/mcp/execution-tools.ts +2009 -0
  760. package/src/runtime/gateway/mcp/persisted-check-process.ts +255 -0
  761. package/src/runtime/gateway/mcp/process-tools.ts +230 -0
  762. package/src/runtime/gateway/mcp/resource-policy.ts +195 -0
  763. package/src/runtime/gateway/mcp/router.ts +1088 -0
  764. package/src/runtime/gateway/mcp/runtime-tools.ts +5994 -0
  765. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +219 -0
  766. package/src/runtime/health/evaluator.ts +474 -0
  767. package/src/runtime/health/index.ts +2 -0
  768. package/src/runtime/health/operational-view.ts +198 -0
  769. package/src/runtime/maintenance/cleanup.ts +419 -0
  770. package/src/runtime/model-clients/deepseek-adapter.ts +312 -0
  771. package/src/runtime/model-clients/index.ts +2 -0
  772. package/src/runtime/model-clients/types.ts +101 -0
  773. package/src/runtime/observability/gateway-contention-metrics.ts +129 -0
  774. package/src/runtime/observability/route-session-benchmark.ts +110 -0
  775. package/src/runtime/personal-assistant/gmail-triage-manager.ts +231 -0
  776. package/src/runtime/personal-assistant/reporting-runtime.ts +344 -0
  777. package/src/runtime/personal-assistant/triage-runtime.ts +748 -0
  778. package/src/runtime/plugins/app-store-connect-adapter.ts +868 -0
  779. package/src/runtime/plugins/browser-adapter.ts +2812 -0
  780. package/src/runtime/plugins/browser-automation-service.ts +46 -0
  781. package/src/runtime/plugins/browser-handoff-host.ts +304 -0
  782. package/src/runtime/plugins/browser-handoff.ts +371 -0
  783. package/src/runtime/plugins/browser-macos-bridge.ts +865 -0
  784. package/src/runtime/plugins/browser-node-bridge-host.ts +64 -0
  785. package/src/runtime/plugins/browser-node-bridge.ts +249 -0
  786. package/src/runtime/plugins/desktop-adapter.ts +374 -0
  787. package/src/runtime/plugins/desktop-operator-registration.ts +418 -0
  788. package/src/runtime/plugins/errors.ts +39 -0
  789. package/src/runtime/plugins/execution-origin.ts +26 -0
  790. package/src/runtime/plugins/external-adapter.ts +322 -0
  791. package/src/runtime/plugins/external-registration.ts +308 -0
  792. package/src/runtime/plugins/external-unix-socket-probe.cjs +79 -0
  793. package/src/runtime/plugins/external-unix-socket.ts +238 -0
  794. package/src/runtime/plugins/first-party-registry.ts +37 -0
  795. package/src/runtime/plugins/github-adapter.ts +266 -0
  796. package/src/runtime/plugins/gmail-adapter.ts +729 -0
  797. package/src/runtime/plugins/google-calendar-adapter.ts +468 -0
  798. package/src/runtime/plugins/google-shared.ts +578 -0
  799. package/src/runtime/plugins/google-tasks-adapter.ts +535 -0
  800. package/src/runtime/plugins/interaction-session.ts +252 -0
  801. package/src/runtime/plugins/ios/agent-device-capabilities.ts +258 -0
  802. package/src/runtime/plugins/ios/agent-device-failures.ts +95 -0
  803. package/src/runtime/plugins/ios/agent-device-provider.ts +63 -0
  804. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +268 -0
  805. package/src/runtime/plugins/ios/app-adapters.ts +122 -0
  806. package/src/runtime/plugins/ios-adapter.ts +509 -0
  807. package/src/runtime/plugins/ios-agent-device.ts +2311 -0
  808. package/src/runtime/plugins/ios-physical-device.ts +955 -0
  809. package/src/runtime/plugins/local-system-adapter.ts +1016 -0
  810. package/src/runtime/plugins/macos-capability-broker.ts +73 -0
  811. package/src/runtime/plugins/managed-process-adapter.ts +366 -0
  812. package/src/runtime/plugins/resend-adapter.ts +506 -0
  813. package/src/runtime/plugins/store.ts +1025 -0
  814. package/src/runtime/plugins/types.ts +138 -0
  815. package/src/runtime/projections/controller-context.ts +596 -0
  816. package/src/runtime/projections/git-status-sampler.ts +84 -0
  817. package/src/runtime/projections/invalidation.ts +250 -0
  818. package/src/runtime/projections/materialized-view.ts +694 -0
  819. package/src/runtime/recovery/actions.ts +158 -0
  820. package/src/runtime/recovery/audit.ts +38 -0
  821. package/src/runtime/recovery/classifier.ts +163 -0
  822. package/src/runtime/recovery/index.ts +9 -0
  823. package/src/runtime/recovery/local-jobs-repair.ts +393 -0
  824. package/src/runtime/recovery/maintenance-executor.ts +822 -0
  825. package/src/runtime/recovery/patch-handoff.ts +50 -0
  826. package/src/runtime/recovery/probe.ts +308 -0
  827. package/src/runtime/recovery/store.ts +35 -0
  828. package/src/runtime/recovery/types.ts +175 -0
  829. package/src/runtime/release/identity.ts +101 -0
  830. package/src/runtime/release/release-gate.ts +137 -0
  831. package/src/runtime/resources/claims/conflicts.ts +205 -0
  832. package/src/runtime/resources/index.ts +1 -0
  833. package/src/runtime/resources/leases/store.ts +539 -0
  834. package/src/runtime/resources/leases/types.ts +50 -0
  835. package/src/runtime/resources/ownership.ts +72 -0
  836. package/src/runtime/root/controller-services.ts +42 -0
  837. package/src/runtime/root/entry.ts +70 -0
  838. package/src/runtime/root/gateway-adapter.ts +46 -0
  839. package/src/runtime/root/local-bridge.ts +72 -0
  840. package/src/runtime/root/mcp-transport.ts +198 -0
  841. package/src/runtime/root/ownership.ts +94 -0
  842. package/src/runtime/root/readiness.ts +69 -0
  843. package/src/runtime/root/release-manifest.ts +147 -0
  844. package/src/runtime/root/release-materialize.ts +509 -0
  845. package/src/runtime/root/release-store.ts +236 -0
  846. package/src/runtime/root/runtime.ts +318 -0
  847. package/src/runtime/root/scheduler.ts +58 -0
  848. package/src/runtime/root/service-runner-entry.ts +20 -0
  849. package/src/runtime/root/service-runner.ts +69 -0
  850. package/src/runtime/root/service.ts +291 -0
  851. package/src/runtime/root/status.ts +127 -0
  852. package/src/runtime/root/types.ts +112 -0
  853. package/src/runtime/root/write-fence.ts +309 -0
  854. package/src/runtime/safe-tooling/browser-safe.ts +133 -0
  855. package/src/runtime/safe-tooling/external-filesystem.ts +304 -0
  856. package/src/runtime/safe-tooling/google-credential-store.ts +95 -0
  857. package/src/runtime/safe-tooling/google-oauth-broker.ts +286 -0
  858. package/src/runtime/safe-tooling/index.ts +11 -0
  859. package/src/runtime/safe-tooling/ios-development.ts +1039 -0
  860. package/src/runtime/safe-tooling/job-summary.ts +361 -0
  861. package/src/runtime/safe-tooling/plugin-summary.ts +53 -0
  862. package/src/runtime/safe-tooling/resend-oauth.ts +387 -0
  863. package/src/runtime/safe-tooling/review-artifacts.ts +133 -0
  864. package/src/runtime/safe-tooling/types.ts +103 -0
  865. package/src/runtime/safe-tooling/workspace-auth.ts +116 -0
  866. package/src/runtime/shared/bounded-child-supervisor.ts +235 -0
  867. package/src/runtime/shared/idempotent-retry.ts +128 -0
  868. package/src/runtime/shared/json-files.ts +43 -0
  869. package/src/runtime/shared/local-bridge-surface.ts +231 -0
  870. package/src/runtime/shared/managed-env.ts +154 -0
  871. package/src/runtime/shared/node-ts-loader.mjs +50 -0
  872. package/src/runtime/shared/process-environment.ts +97 -0
  873. package/src/runtime/shared/process-identity.ts +72 -0
  874. package/src/runtime/shared/process-tree.ts +227 -0
  875. package/src/runtime/shared/response-budget.ts +246 -0
  876. package/src/runtime/standalone-recovery/core.ts +2275 -0
  877. package/src/runtime/standalone-recovery/entry.ts +686 -0
  878. package/src/runtime/standalone-recovery/http-transport.ts +317 -0
  879. package/src/runtime/standalone-recovery/installer.ts +747 -0
  880. package/src/runtime/standalone-recovery/release.ts +241 -0
  881. package/src/runtime/watchdog/workflow-watchdog.ts +118 -0
  882. package/src/runtime/workflow/campaigns/cleanup.ts +263 -0
  883. package/src/runtime/workflow/campaigns/engine.ts +112 -0
  884. package/src/runtime/workflow/campaigns/normalize.ts +100 -0
  885. package/src/runtime/workflow/campaigns/review.ts +315 -0
  886. package/src/runtime/workflow/campaigns/store.ts +526 -0
  887. package/src/runtime/workflow/campaigns/supervisor.ts +112 -0
  888. package/src/runtime/workflow/campaigns/types.ts +290 -0
  889. package/src/runtime/workflow/campaigns/workspace-agent.ts +155 -0
  890. package/src/runtime/workflow/campaigns/workspace.ts +200 -0
  891. package/src/runtime/workflow/findings/store.ts +165 -0
  892. package/src/runtime/workflow/findings/types.ts +36 -0
  893. package/src/runtime/workflow/portfolio/engine.ts +20 -0
  894. package/src/runtime/workflow/portfolio/store.ts +185 -0
  895. package/src/runtime/workflow/portfolio/types.ts +29 -0
  896. package/src/runtime/workflow/schedules/engine.ts +620 -0
  897. package/src/runtime/workflow/schedules/settlement.ts +144 -0
  898. package/src/runtime/workflow/schedules/store.ts +417 -0
  899. package/src/runtime/workflow/schedules/types.ts +124 -0
  900. package/src/runtime/workspace-targets/index.ts +24 -0
  901. package/src/runtime/workspace-targets/target-grant.ts +721 -0
  902. package/src/testing/test-governance.ts +626 -0
  903. package/src/version.ts +21 -0
@@ -0,0 +1,3183 @@
1
+ import { randomBytes } from "crypto";
2
+ import { spawn } from "child_process";
3
+ import { createServer, type Server } from "http";
4
+ import express, {
5
+ type NextFunction,
6
+ type Request,
7
+ type Response,
8
+ } from "express";
9
+ import {
10
+ cancelAgentJob,
11
+ getAgentJob,
12
+ getAgentJobEvents,
13
+ getAgentJobLog,
14
+ listAgentJobs,
15
+ reconcileAgentJobs,
16
+ } from "../agent-jobs/job-manager";
17
+ import {
18
+ cleanupIntegratedWorktree,
19
+ integrateAgentJob,
20
+ taskRunDiff,
21
+ } from "../agent-jobs/integration";
22
+ import { listControllerChecks, runControllerCheck, runControllerCheckAsync } from "../controller/check-runner";
23
+ import {
24
+ acceptVerifiedTask,
25
+ archiveIssue,
26
+ getIssue,
27
+ inspectIssueReadiness,
28
+ inspectTaskReadiness,
29
+ projectBoard,
30
+ restoreIssue,
31
+ recordTaskVerification,
32
+ setTaskDependencies,
33
+ updateTask,
34
+ } from "../controller/issue-store";
35
+ import { readTaskRunEvidence } from "../controller/run-evidence";
36
+ import { resolveEffectiveTaskState } from "../controller/task-status-resolver";
37
+ import {
38
+ getControllerTimeline,
39
+ getProjectProgress,
40
+ getTaskProgressDetail,
41
+ } from "../controller/progress";
42
+ import { buildControllerTaskLedgerProjection } from "../controller/task-ledger";
43
+ import { buildControllerOperationalPlan } from "../controller/operational-plan";
44
+ import { exportControllerWorklog, listControllerWorklogEvents, parseWorklogCategory } from "../controller/worklog";
45
+ import { inspectProjectGovernance, reconcileProjectGovernance } from "../controller/governance";
46
+ import { clearCurrentIssue, loadControllerProjectState, saveControllerProjectState } from "../controller/project-state";
47
+ import { legacyIssueAuthorityRetired } from "../controller/legacy-issue-cutover";
48
+ import { buildExecutionDiagnostics, buildRequirementBoard } from "../../runtime/control-plane/facade/requirement-board";
49
+ import {
50
+ closeIssueWithGitHubPlugin,
51
+ getGitHubPluginStatus,
52
+ publishIssueWithGitHubPlugin,
53
+ refreshIssueWithGitHubPlugin,
54
+ saveGitHubPluginConfig,
55
+ } from "../github/plugin";
56
+ import {
57
+ cancelLocalBridgeJob,
58
+ executeLocalBridgeJob,
59
+ getLocalBridgeJob,
60
+ getLocalBridgeJobEvents,
61
+ listLocalBridgeJobs,
62
+ localBridgeTimeoutPolicy,
63
+ reconcileLocalBridgeJobs,
64
+ } from "./job-store";
65
+ import {
66
+ createEditSavepoint,
67
+ finalizeEditSession,
68
+ getEditSession,
69
+ getEditSessionDiff,
70
+ listEditSessions,
71
+ rollbackEditSession,
72
+ verifyEditSession,
73
+ } from "../editing/edit-session";
74
+ import { localBridgeDashboardHtml } from "./dashboard";
75
+ import {
76
+ ackConsoleHandoff,
77
+ approveConsoleHandoff,
78
+ applyConsoleSafePatch,
79
+ buildAdvancedDiagnosticsEnvelope,
80
+ buildCommandCenter,
81
+ buildPluginSummary,
82
+ buildSystemReadiness,
83
+ consoleGoalLoopPolicyGet,
84
+ consoleGoalLoopPolicyUpdate,
85
+ consoleLocalToolConfigGet,
86
+ consoleLocalToolConfigUpdate,
87
+ consoleLocalToolDisable,
88
+ consoleLocalToolEnable,
89
+ consoleLocalToolHealth,
90
+ consoleLocalToolList,
91
+ consoleProviderApiSettingsGet,
92
+ consoleProviderApiSettingsUpdate,
93
+ consoleProviderConfigGet,
94
+ consoleProviderConfigUpdate,
95
+ consoleProviderCredentials,
96
+ consoleProviderDisable,
97
+ consoleProviderEnable,
98
+ consoleProviderHealth,
99
+ consoleProviderPriority,
100
+ consoleProviderReset,
101
+ consoleRoutePreview,
102
+ consoleRoutingGet,
103
+ consoleRoutingReset,
104
+ consoleRoutingUpdate,
105
+ evaluateConsoleConnectorFreshness,
106
+ getAutomationSettings,
107
+ getConsoleAccessPolicy,
108
+ getConsolePlugin,
109
+ listConsolePlugins,
110
+ toConsoleOperationFeedback,
111
+ continueConsoleWork,
112
+ delegateConsoleWork,
113
+ dismissConsoleHandoff,
114
+ finalizeConsoleWork,
115
+ getConsoleHandoff,
116
+ getConsoleWork,
117
+ listConsoleHandoffs,
118
+ listConsoleWork,
119
+ mapRepositoryCard,
120
+ previewExecutionMode,
121
+ repairConsole,
122
+ resolveConsoleHandoff,
123
+ setConsoleAccessPolicy,
124
+ startConsoleWork,
125
+ stopConsoleWork,
126
+ verifyConsoleWork,
127
+ } from "./facade-api";
128
+ import {
129
+ FORGE_MCP_SCHEMA_VERSION,
130
+ FORGE_TOOL_SURFACE,
131
+ FORGE_VERSION,
132
+ forgeToolSurfaceFingerprint,
133
+ defaultLocalAgentRunners,
134
+ } from "../controller/runtime-config";
135
+ import { taskExecutionPolicy, taskWriteScopesConflict } from "../controller/execution-policy";
136
+ import { continueTaskAfterSuccessfulRun } from "../controller/execution-completion";
137
+ import { applyCompletionDecision, completionDecisionQueues, finishCompletionBacklog, inspectCompletionBacklog } from "../controller/completion-backlog";
138
+ import { finishEditSession, finishTaskRun } from "../controller/completion-orchestrator";
139
+ import { prepareCodexContinuation } from "../controller/codex-continuation";
140
+ import { applyStuckStateMigration, inspectStuckControllerStates } from "../controller/stuck-state-migration";
141
+ import { getMcpPolicy } from "../mcp/policy";
142
+ import type { MultiRepositoryMcpToolContext } from "../mcp/multi-repository";
143
+ import { createMcpToolContext as createControllerMcpToolContext } from "../mcp/server";
144
+ import { loadMcpLocalConfig, loadMcpServiceLocalConfig, loadMcpServiceRuntimeState } from "../mcp/auth";
145
+ import { loadRepositoryRegistry, registerRepository, removeRepository, resolveRepositorySelection } from "../repositories/registry";
146
+ import { resolveControllerHome, resolveRepoPreferredControllerHome } from "../repositories/controller-home";
147
+ import { ensureRepositoryRuntimeStorage } from "../repositories/runtime-storage";
148
+ import { readForgeRuntimeStatus } from "../../runtime/control-plane/runtime-status-client";
149
+ import { readRuntimeGeneration } from "../../runtime/control-plane/runtime-generation";
150
+ import { findExecutionJob, listExecutionJobs } from "../../runtime/execution/jobs/store";
151
+ import { rebuildRepositoryProjection, readRepositoryProjectionSnapshot } from "../../runtime/projections/materialized-view";
152
+ import { getAssistantPluginManifest, listAssistantPluginManifests, submitAssistantPluginAction, findPluginActionReceipt, compatibilityPluginJobFromReceipt} from "../../runtime/plugins/store";
153
+ import {
154
+ createMobileIntentDevice,
155
+ listMobileIntentDevices,
156
+ mobileIntentHasScope,
157
+ revokeMobileIntentDevice,
158
+ verifyMobileIntentRequest,
159
+ } from "./mobile-intents";
160
+ import { buildMobileMonitorSnapshot } from "./mobile-monitor";
161
+ import { controllerExposureSnapshot } from "../mcp/toolset";
162
+ import { submitAssistantIntent, runAssistantRoutineNow } from "../../runtime/assistant/intent";
163
+ import { updateAssistantRoutineLifecycle } from "../../runtime/assistant/schedule-binding";
164
+ import { assistantOpenApiSchema } from "../../runtime/assistant/openapi";
165
+ import { completeGoogleOAuthLogin } from "../../runtime/safe-tooling/google-oauth-broker";
166
+ import { completeResendOAuthLogin } from "../../runtime/safe-tooling/resend-oauth";
167
+ import { approveAssistantActionProposal, listAssistantActionProposals, rejectAssistantActionProposal } from "../../runtime/assistant/action-proposals";
168
+ import { assistantModelReadiness } from "../../runtime/assistant/model-provider";
169
+ import { createAssistantStandingGrant, listAssistantStandingGrants, revokeAssistantStandingGrant } from "../../runtime/assistant/standing-grants";
170
+ import { buildAssistantReadinessReport } from "../../runtime/assistant/readiness";
171
+ import { listWebTargets, previewBrowserDomainAccess, summarizePluginForLowInterception } from "../../runtime/safe-tooling";
172
+ import { buildModelClientSummary, buildModelControlPlaneSummary, deepSeekControllerManifest, deepSeekFunctionToolManifest, prepareDeepSeekControllerHandoff, prepareDeepSeekControllerRequest, prepareDeepSeekToolCall } from "../../runtime/model-clients";
173
+ import { applyRuntimeCleanup, previewRuntimeCleanup } from "../../runtime/maintenance/cleanup";
174
+ import { assertRecoveryAuthorized, buildCapabilityRecoverySnapshot, buildRecoveryAuditRecord, recoveryActionById, writeRecoveryAuditRecord } from "../../runtime/recovery";
175
+ import {
176
+ listAssistantInbox,
177
+ listAssistantMemory,
178
+ listAssistantRoutines,
179
+ updateAssistantInboxStatus,
180
+ upsertAssistantMemory,
181
+ } from "../../runtime/assistant/store";
182
+
183
+ export interface LocalBridgeServerOptions {
184
+ /**
185
+ * Compatibility root used to bootstrap older repo-local storage. New Local
186
+ * Bridge endpoints should resolve the active repository through repoId and
187
+ * use repoRoot only as a fallback/default selection.
188
+ */
189
+ repoRoot: string;
190
+ controllerHome?: string;
191
+ defaultRepoId?: string;
192
+ host?: string;
193
+ port?: number;
194
+ openBrowser?: boolean;
195
+ token?: string;
196
+ allowLanMobileIntents?: boolean;
197
+ mode?: "standalone" | "embedded" | "remote" | "disabled" | "unknown";
198
+ slot?: "blue" | "green";
199
+ }
200
+
201
+ export interface LocalBridgeServerHandle {
202
+ host: string;
203
+ port: number;
204
+ url: string;
205
+ token: string;
206
+ server: Server;
207
+ close(): Promise<void>;
208
+ }
209
+
210
+ function normalizeHostname(hostname: string): string {
211
+ return hostname.replace(/^\[|\]$/g, "").toLowerCase();
212
+ }
213
+
214
+ function isLoopbackHostname(hostname: string): boolean {
215
+ return ["127.0.0.1", "localhost", "::1"].includes(normalizeHostname(hostname));
216
+ }
217
+
218
+ function isWildcardBindHost(host: string): boolean {
219
+ return ["0.0.0.0", "::", "[::]"].includes(normalizeHostname(host));
220
+ }
221
+
222
+ function assertAllowedBindHost(host: string, allowLanMobileIntents: boolean): void {
223
+ if (isLoopbackHostname(host)) return;
224
+ if (allowLanMobileIntents && isWildcardBindHost(host)) return;
225
+ throw new Error(
226
+ `local controller must bind to loopback unless --mobile-lan is enabled, received: ${host}`,
227
+ );
228
+ }
229
+
230
+ function parsedRequestHost(value: string | undefined): URL | undefined {
231
+ if (!value) return undefined;
232
+ try {
233
+ return new URL(`http://${value}`);
234
+ } catch (_error) {
235
+ return undefined;
236
+ }
237
+ }
238
+
239
+ function cookieValue(request: Request, name: string): string | undefined {
240
+ const raw = request.headers.cookie;
241
+ if (!raw) return undefined;
242
+ for (const part of raw.split(";")) {
243
+ const [key, ...valueParts] = part.trim().split("=");
244
+ if (key !== name) continue;
245
+ try {
246
+ return decodeURIComponent(valueParts.join("="));
247
+ } catch (_error) {
248
+ return undefined;
249
+ }
250
+ }
251
+ return undefined;
252
+ }
253
+
254
+ function openUrl(url: string): void {
255
+ const command =
256
+ process.platform === "darwin"
257
+ ? "open"
258
+ : process.platform === "win32"
259
+ ? "cmd"
260
+ : "xdg-open";
261
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
262
+ try {
263
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
264
+ child.unref();
265
+ } catch (_error) {
266
+ // The URL is still printed by the caller when a desktop opener is unavailable.
267
+ }
268
+ }
269
+
270
+ function errorMessage(error: unknown): string {
271
+ return error instanceof Error ? error.message : String(error);
272
+ }
273
+
274
+ function localBridgeExecutionRetiredPayload(): Record<string, unknown> {
275
+ return {
276
+ error: "LOCAL_BRIDGE_JOB_RETIRED",
277
+ message: "New Local Bridge Jobs are retired. Claim Work through an external Controller, or use the Process Runtime for deterministic commands and checks.",
278
+ };
279
+ }
280
+
281
+ function legacyControlPlaneMutationRetiredPayload(): Record<string, unknown> {
282
+ return {
283
+ error: "LEGACY_CONTROL_PLANE_MUTATION_RETIRED",
284
+ message: "Issue, Task, currentIssue and legacy project-board mutation APIs are frozen after SQLite cutover. Use Requirement, Plan and Work APIs.",
285
+ authority: "controller-home-sqlite",
286
+ deprecated: true,
287
+ frozen: true,
288
+ };
289
+ }
290
+
291
+ function queryString(value: unknown): string | undefined {
292
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
293
+ }
294
+
295
+ function controllerStateSignature(repoRoot: string): string {
296
+ const snapshot = cachedLocalControllerSnapshot(repoRoot);
297
+ const latestWorklog = listControllerWorklogEvents(repoRoot, { limit: 1 })[0];
298
+ return JSON.stringify({
299
+ issues: snapshot.board.issues.map((issue) => ({
300
+ id: issue.id,
301
+ status: issue.status,
302
+ updatedAt: issue.updatedAt,
303
+ tasks: (Array.isArray(issue.tasks) ? issue.tasks : []).map((task) => {
304
+ const value = task as { id?: unknown; status?: unknown; updatedAt?: unknown; runIds?: unknown[] };
305
+ return [value.id, value.status, value.updatedAt, Array.isArray(value.runIds) ? value.runIds.at(-1) : undefined];
306
+ }),
307
+ })),
308
+ runs: snapshot.runs.map((run) => [
309
+ run.runId,
310
+ run.status,
311
+ run.lastHeartbeatAt,
312
+ run.progress?.lastActivityAt,
313
+ run.integratedAt,
314
+ run.autoIntegrationError,
315
+ ]),
316
+ jobs: snapshot.localJobs.map((job) => [job.jobId, job.status, job.updatedAt, job.runId]),
317
+ worklog: latestWorklog?.id,
318
+ projectState: snapshot.projectState,
319
+ edits: snapshot.editSessions.map((edit) => [edit.sessionId, edit.status, edit.updatedAt, edit.changedFiles, edit.checksPassed, edit.checksTotal]),
320
+ });
321
+ }
322
+
323
+ const SNAPSHOT_CACHE_TTL_MS = 2_000;
324
+ const localSnapshotCache = new Map<string, { createdAt: number; value: ReturnType<typeof buildLocalControllerSnapshot> }>();
325
+
326
+ function cachedLocalControllerSnapshot(repoRoot: string): ReturnType<typeof buildLocalControllerSnapshot> {
327
+ const now = Date.now();
328
+ const cached = localSnapshotCache.get(repoRoot);
329
+ if (cached && now - cached.createdAt <= SNAPSHOT_CACHE_TTL_MS) return cached.value;
330
+ const value = buildLocalControllerSnapshot(repoRoot);
331
+ localSnapshotCache.set(repoRoot, { createdAt: now, value });
332
+ return value;
333
+ }
334
+
335
+ function runtimeControllerSnapshot(repoRoot: string) {
336
+ const controllerHome = resolveControllerHome();
337
+ const normalizedRoot = repoRoot.replace(/\\/g, '/');
338
+ const repository = loadRepositoryRegistry(controllerHome).repositories.find((entry) => entry.canonicalRoot.replace(/\\/g, '/') === normalizedRoot);
339
+ if (!repository) return { registered: false, daemon: readForgeRuntimeStatus(controllerHome) };
340
+ return {
341
+ registered: true,
342
+ repoId: repository.repoId,
343
+ checkoutId: repository.activeCheckoutId,
344
+ daemon: readForgeRuntimeStatus(controllerHome),
345
+ projection: readRepositoryProjectionSnapshot(controllerHome, repository.repoId).projection,
346
+ executionJobs: listExecutionJobs(controllerHome, repository.repoId, 30),
347
+ };
348
+ }
349
+
350
+ export function buildLocalControllerSnapshot(repoRoot: string) {
351
+ const retiredLegacyControlPlane = legacyIssueAuthorityRetired(repoRoot);
352
+ const authoritativeControllerHome = resolveRepoPreferredControllerHome(repoRoot);
353
+ const runtimeRepository = loadRepositoryRegistry(authoritativeControllerHome).repositories.find(
354
+ (entry) => entry.canonicalRoot.replace(/\\/g, '/') === repoRoot.replace(/\\/g, '/'),
355
+ );
356
+ const requirementBoard = retiredLegacyControlPlane
357
+ ? buildRequirementBoard({ controllerHome: authoritativeControllerHome, repoId: runtimeRepository?.repoId })
358
+ : undefined;
359
+ const executionDiagnostics = retiredLegacyControlPlane
360
+ ? buildExecutionDiagnostics({ controllerHome: authoritativeControllerHome, repoId: runtimeRepository?.repoId, detailLevel: "detail" })
361
+ : undefined;
362
+ const runs = listAgentJobs(repoRoot, 30);
363
+ const editSessions = listEditSessions(repoRoot, 30);
364
+ const localJobs = listLocalBridgeJobs(repoRoot, 30);
365
+ const board = retiredLegacyControlPlane
366
+ ? {
367
+ schemaVersion: 1,
368
+ projection: "deprecated_frozen_legacy_board",
369
+ authority: "controller-home-sqlite",
370
+ deprecated: true,
371
+ frozen: true,
372
+ issues: [],
373
+ counts: requirementBoard?.counts ?? {},
374
+ declaredCounts: {},
375
+ archivedCounts: {},
376
+ readyTasks: [],
377
+ queueableTasks: [],
378
+ archivedIssueCount: 0,
379
+ notice: "Requirement Board is authoritative. Legacy Issue/Task board data is intentionally omitted.",
380
+ }
381
+ : projectBoard(repoRoot);
382
+ const taskLedger = buildControllerTaskLedgerProjection(repoRoot);
383
+ const operationalPlan = buildControllerOperationalPlan(repoRoot, taskLedger);
384
+ const completionBacklog = retiredLegacyControlPlane
385
+ ? { scannedAt: new Date().toISOString(), counts: {}, finishableRunIds: [], needsHumanReviewRunIds: [], retryTaskRefs: [], items: [], recommendations: ["Use Work and Execution Diagnostics."] }
386
+ : inspectCompletionBacklog(repoRoot, { limit: 100 });
387
+ const completionQueues = retiredLegacyControlPlane
388
+ ? { scannedAt: new Date().toISOString(), counts: {}, autoFinish: [], needsHumanReview: [], retryRequired: [], noRunEvidence: [], systemBlocked: [], recentlyTerminal: [], recommendations: ["Use Work and Execution Diagnostics."] }
389
+ : completionDecisionQueues(repoRoot, { limit: 100 });
390
+ const stuckStates = retiredLegacyControlPlane
391
+ ? { generatedAt: new Date().toISOString(), findings: [], counts: {}, recommendations: ["Use Work and Execution Diagnostics."] }
392
+ : inspectStuckControllerStates(repoRoot, { limit: 100 });
393
+ const runtime = runtimeControllerSnapshot(repoRoot);
394
+ const executionJobs = "executionJobs" in runtime ? (runtime.executionJobs ?? []) : [];
395
+ const controllerHome = resolveControllerHome();
396
+ const normalizedAssistantRoot = repoRoot.replace(/\\/g, '/');
397
+ const assistantRepository = loadRepositoryRegistry(controllerHome).repositories.find(
398
+ (entry) => entry.canonicalRoot.replace(/\\/g, '/') === normalizedAssistantRoot,
399
+ );
400
+ const assistantPlugins = (assistantRepository ? listAssistantPluginManifests(controllerHome, assistantRepository, {
401
+ preferStored: true,
402
+ }) : []).map((plugin) => ({
403
+ pluginId: plugin.pluginId,
404
+ provider: plugin.provider,
405
+ displayName: plugin.displayName,
406
+ enabled: plugin.enabled,
407
+ revision: plugin.revision,
408
+ lifecycle: plugin.lifecycle,
409
+ health: plugin.health,
410
+ permissions: plugin.permissions,
411
+ actions: plugin.actions.map((action) => ({
412
+ actionId: action.actionId,
413
+ title: action.title,
414
+ readOnly: action.readOnly,
415
+ risk: action.risk,
416
+ confirmation: action.confirmation,
417
+ requiredConfirmationText: action.requiredConfirmationText,
418
+ scopes: action.scopes,
419
+ })),
420
+ }));
421
+ const mobileIntents = listMobileIntentDevices(repoRoot);
422
+ const assistantInbox = listAssistantInbox(repoRoot, 20);
423
+ const assistantRoutines = listAssistantRoutines(repoRoot);
424
+ const assistantMemory = listAssistantMemory(repoRoot);
425
+ const boardIssues = board.issues as Array<{
426
+ id: string;
427
+ tasks?: Array<{ id: string; title: string; effectiveStatus: string }>;
428
+ }>;
429
+ const reviewTasks = boardIssues.flatMap((issue) => (issue.tasks ?? [])
430
+ .filter((task) => ["review", "verified", "changes_requested"].includes(task.effectiveStatus))
431
+ .map((task) => ({ kind: "task", issueId: issue.id, taskId: task.id, title: task.title, status: task.effectiveStatus })));
432
+ const decisionQueues = {
433
+ needsAttention: [
434
+ ...runs.filter((run) => ["failed", "waiting_for_user", "unknown"].includes(run.status))
435
+ .map((run) => ({ kind: "run", id: run.runId, title: run.progress?.currentActivity ?? run.runId, status: run.status, updatedAt: run.lastHeartbeatAt })),
436
+ ...editSessions.filter((session) => session.status === "check_failed")
437
+ .map((session) => ({ kind: "edit", id: session.sessionId, title: session.purpose, status: session.status, updatedAt: session.updatedAt })),
438
+ ...executionJobs.filter((job) => ["human_attention_required", "failed", "orphaned", "stale"].includes(job.status))
439
+ .map((job) => ({ kind: "work", id: job.jobId, title: String(job.payload.operation ?? job.type), status: job.status, updatedAt: job.updatedAt })),
440
+ ].slice(0, 12),
441
+ runningNow: [
442
+ ...runs.filter((run) => ["queued", "starting", "running"].includes(run.status))
443
+ .map((run) => ({ kind: "run", id: run.runId, title: run.progress?.currentActivity ?? run.runId, status: run.status, updatedAt: run.lastHeartbeatAt })),
444
+ ...editSessions.filter((session) => !["finalized", "rolled_back", "check_failed"].includes(session.status))
445
+ .map((session) => ({ kind: "edit", id: session.sessionId, title: session.purpose, status: session.status, updatedAt: session.updatedAt })),
446
+ ...executionJobs.filter((job) => ["queued", "dispatched", "running", "waiting_for_dependency", "waiting_for_workspace", "waiting_for_heavy_check", "waiting_for_integration"].includes(job.status))
447
+ .map((job) => ({ kind: "work", id: job.jobId, title: String(job.payload.operation ?? job.type), status: job.status, updatedAt: job.updatedAt })),
448
+ ].slice(0, 12),
449
+ readyForReview: [
450
+ ...completionQueues.autoFinish.map((item) => ({ kind: "completion", id: item.runId ?? `${item.issueId}/${item.taskId}`, runId: item.runId, issueId: item.issueId, taskId: item.taskId, title: item.title, status: item.action, updatedAt: undefined })),
451
+ ...reviewTasks,
452
+ ].slice(0, 12),
453
+ pendingApprovals: [
454
+ ...completionQueues.needsHumanReview.map((item) => ({ kind: "completion-review", id: item.runId ?? `${item.issueId}/${item.taskId}`, runId: item.runId, issueId: item.issueId, taskId: item.taskId, title: item.title, status: item.action, updatedAt: undefined })),
455
+ ...localJobs.filter((job) => job.status === "pending_approval")
456
+ .map((job) => ({ kind: "local-job", id: job.jobId, title: job.action, status: job.status, updatedAt: job.updatedAt })),
457
+ ...executionJobs.filter((job) => job.status === "human_attention_required")
458
+ .map((job) => ({ kind: "work", id: job.jobId, title: String(job.payload.operation ?? job.type), status: job.status, updatedAt: job.updatedAt })),
459
+ ].slice(0, 12),
460
+ recentlyCompleted: [
461
+ ...runs.filter((run) => ["succeeded", "cancelled"].includes(run.status))
462
+ .map((run) => ({ kind: "run", id: run.runId, title: run.progress?.currentActivity ?? run.runId, status: run.status, updatedAt: run.finishedAt ?? run.lastHeartbeatAt })),
463
+ ...editSessions.filter((session) => ["finalized", "rolled_back"].includes(session.status))
464
+ .map((session) => ({ kind: "edit", id: session.sessionId, title: session.purpose, status: session.status, updatedAt: session.updatedAt })),
465
+ ...executionJobs.filter((job) => ["succeeded", "cancelled", "timed_out"].includes(job.status))
466
+ .map((job) => ({ kind: "work", id: job.jobId, title: String(job.payload.operation ?? job.type), status: job.status, updatedAt: job.updatedAt })),
467
+ ].sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? ""))).slice(0, 12),
468
+ };
469
+ const mcpRuntime = loadMcpServiceRuntimeState(controllerHome, repoRoot);
470
+ const mcpConfig = loadMcpServiceLocalConfig(controllerHome, repoRoot);
471
+ const mcpExposureContext = createControllerMcpToolContext({
472
+ repo: repoRoot,
473
+ controllerHome,
474
+ profile: "controller",
475
+ }) as MultiRepositoryMcpToolContext;
476
+ const exposure = controllerExposureSnapshot(mcpExposureContext);
477
+ // mcp.runtime.json is lifecycle diagnostics only. In particular, its server
478
+ // fields may describe a Gateway process from a prior Runtime release and
479
+ // must never be treated as the Connector's callable schema.
480
+ const CONNECTOR_SCHEMA_UNVERIFIED_MESSAGE = 'Connector schema is unverified until live MCP discovery completes.';
481
+ const expectedFingerprint = exposure.fingerprint;
482
+ // Snapshot construction is synchronous. Do not infer connector health from
483
+ // mcp.runtime.json; only an async live MCP discovery can establish it.
484
+ const connectorHealthy = undefined;
485
+ const runtimeProjection = "projection" in runtime ? runtime.projection : undefined;
486
+ const recovery = buildCapabilityRecoverySnapshot({
487
+ daemonStatus: runtime.daemon.status,
488
+ daemonError: runtime.daemon.error,
489
+ schedulerStatus: runtimeProjection ? (runtimeProjection.queueDepth > 0 && runtimeProjection.runningWorkers === 0 ? "degraded" : "ready") : undefined,
490
+ queueDepth: runtimeProjection?.queueDepth,
491
+ runningWorkers: runtimeProjection?.runningWorkers,
492
+ activeLeases: runtimeProjection?.activeLeases,
493
+ localBridgeRunning: true,
494
+ connectorHealthy,
495
+ connectorMismatch: CONNECTOR_SCHEMA_UNVERIFIED_MESSAGE,
496
+ runtimeProjectionStale: false,
497
+ runtimeProjectionPersisted: Boolean(runtimeProjection),
498
+ commandPreviewAvailable: true,
499
+ commandExecuteAvailable: true,
500
+ issueToolsAvailable: !retiredLegacyControlPlane,
501
+ jobToolsAvailable: true,
502
+ checksAvailable: listControllerChecks(repoRoot).length > 0,
503
+ pluginStates: assistantPlugins.map((plugin) => ({
504
+ pluginId: plugin.pluginId,
505
+ enabled: plugin.enabled,
506
+ healthState: plugin.health.state,
507
+ ready: plugin.health.ready,
508
+ errors: plugin.health.errors,
509
+ warnings: plugin.health.warnings,
510
+ })),
511
+ recentErrors: [
512
+ ...localJobs.flatMap((job) => job.error ? [job.error] : []),
513
+ ...executionJobs.flatMap((job) => job.error?.message ? [job.error.message] : []),
514
+ ],
515
+ localJobs: localJobs.map((job) => ({ status: job.status, error: job.error, updatedAt: job.updatedAt })),
516
+ executionJobs: executionJobs.map((job) => ({ status: job.status, error: job.error, updatedAt: job.updatedAt, operation: job.payload.operation })),
517
+ assistant: {
518
+ inboxCount: assistantInbox.items.length,
519
+ routineCount: assistantRoutines.routines.length,
520
+ memoryCount: assistantMemory.entries.length,
521
+ },
522
+ });
523
+ return {
524
+ generatedAt: new Date().toISOString(),
525
+ repoRoot,
526
+ toolSurface: FORGE_TOOL_SURFACE,
527
+ schemaVersion: FORGE_MCP_SCHEMA_VERSION,
528
+ version: FORGE_VERSION,
529
+ toolSurfaceFingerprint: expectedFingerprint,
530
+ connector: {
531
+ configuredServerName: mcpConfig?.chatgpt?.serverName,
532
+ publicEndpoint:
533
+ mcpConfig?.chatgpt?.endpoint ?? mcpRuntime?.tunnel?.publicEndpoint,
534
+ runtimeStatus: mcpRuntime?.status ?? "not_started",
535
+ runtimeProfile: null,
536
+ runtimeSurface: null,
537
+ runtimeSchemaVersion: null,
538
+ runtimeForgeVersion: null,
539
+ runtimeFingerprint: null,
540
+ expectedFingerprint,
541
+ runtimeToolset: null,
542
+ configuredToolset: exposure.access.effectiveToolset,
543
+ configuredAccessMode: exposure.access.configuredAccessMode,
544
+ effectiveAccessMode: exposure.access.effectiveAccessMode,
545
+ exposureRevision: exposure.access.exposureRevision,
546
+ runtimeToolFingerprint: null,
547
+ expectedRuntimeFingerprint: expectedFingerprint,
548
+ toolCount: null,
549
+ healthy: connectorHealthy,
550
+ needsReconnect: mcpRuntime?.tunnel?.connectorNeedsReconnect === true,
551
+ mismatch: CONNECTOR_SCHEMA_UNVERIFIED_MESSAGE,
552
+ },
553
+ timeoutPolicy: localBridgeTimeoutPolicy(repoRoot),
554
+ runtime,
555
+ decisionQueues,
556
+ completion: {
557
+ backlog: completionBacklog,
558
+ queues: completionQueues,
559
+ stuckStates,
560
+ },
561
+ execution: {
562
+ defaultMode: "direct-edit",
563
+ agentRunner: mcpConfig?.devMode?.agentRunner === true,
564
+ allowedAgents: mcpConfig?.devMode?.allowedAgents ?? defaultLocalAgentRunners(),
565
+ taskAgentBinding: false,
566
+ localRiskApprovalGate: false,
567
+ },
568
+ requirementBoard,
569
+ executionDiagnostics,
570
+ legacyCompatibility: retiredLegacyControlPlane ? {
571
+ authority: "controller-home-sqlite",
572
+ projection: "deprecated_frozen",
573
+ mutationApis: "retired",
574
+ } : undefined,
575
+ board,
576
+ taskLedger,
577
+ operationalPlan,
578
+ projectState: loadControllerProjectState(repoRoot),
579
+ governance: inspectProjectGovernance(repoRoot),
580
+ recovery,
581
+ progress: getProjectProgress(repoRoot),
582
+ timeline: getControllerTimeline(repoRoot, { limit: 40 }),
583
+ githubPlugin: getGitHubPluginStatus(repoRoot),
584
+ assistantPlugins,
585
+ assistant: {
586
+ inbox: assistantInbox.items,
587
+ routines: assistantRoutines.routines,
588
+ memory: assistantMemory.entries,
589
+ },
590
+ mobileIntents,
591
+ runs,
592
+ runCounts: runs.reduce<Record<string, number>>((counts, run) => {
593
+ counts[run.status] = (counts[run.status] ?? 0) + 1;
594
+ return counts;
595
+ }, {}),
596
+ localJobs,
597
+ editSessions,
598
+ checks: listControllerChecks(repoRoot),
599
+ };
600
+ }
601
+
602
+ type LocalControllerSnapshot = ReturnType<typeof buildLocalControllerSnapshot>;
603
+
604
+ interface UserFacingAction {
605
+ id: string;
606
+ label: string;
607
+ }
608
+
609
+ interface UserFacingAttentionItem {
610
+ id: string;
611
+ category: string;
612
+ categoryLabel: string;
613
+ title: string;
614
+ reason: string;
615
+ scope: string;
616
+ icon: string;
617
+ primaryAction: UserFacingAction;
618
+ refs?: Record<string, string | undefined>;
619
+ }
620
+
621
+ function normalizePathForCompare(value: string | undefined): string {
622
+ return String(value ?? '').replace(/\\/g, '/');
623
+ }
624
+
625
+ function displayPath(value: string | undefined): string {
626
+ const raw = String(value ?? '');
627
+ const home = process.env.HOME;
628
+ return home && raw.startsWith(home) ? `~${raw.slice(home.length)}` : raw;
629
+ }
630
+
631
+ function action(id: string, label: string): UserFacingAction {
632
+ return { id, label };
633
+ }
634
+
635
+ function userFacingRepositories(repoRoot: string, controllerHome = resolveControllerHome(), selectedRepoId?: string) {
636
+ const currentRoot = normalizePathForCompare(repoRoot);
637
+ return loadRepositoryRegistry(controllerHome).repositories
638
+ .filter((record) => !record.removedAt)
639
+ .map((record) => {
640
+ const checkout = record.checkouts.find((value) => value.checkoutId === record.activeCheckoutId) ?? record.checkouts[0];
641
+ const current = selectedRepoId
642
+ ? record.repoId === selectedRepoId
643
+ : normalizePathForCompare(record.canonicalRoot) === currentRoot || normalizePathForCompare(checkout?.canonicalRoot) === currentRoot;
644
+ const status = record.enabled === false ? 'disabled' : current ? 'ready' : 'available';
645
+ return {
646
+ id: record.repoId,
647
+ name: record.displayName,
648
+ path: displayPath(checkout?.canonicalRoot ?? record.canonicalRoot),
649
+ status,
650
+ statusLabel: status === 'ready' ? '就绪' : status === 'available' ? '可用' : '已停用',
651
+ current,
652
+ defaultBranch: record.defaultBranch,
653
+ remote: record.remoteUrl,
654
+ updatedAt: record.updatedAt,
655
+ lastSeenAt: record.lastSeenAt,
656
+ };
657
+ })
658
+ .sort((left, right) => Number(right.current) - Number(left.current) || String(right.lastSeenAt ?? '').localeCompare(String(left.lastSeenAt ?? '')));
659
+ }
660
+
661
+ function requestRepositorySelection(request: Request, options: LocalBridgeServerOptions, controllerHome: string) {
662
+ const body = request.body && typeof request.body === 'object' && !Array.isArray(request.body)
663
+ ? request.body as Record<string, unknown>
664
+ : {};
665
+ const repoId = queryString(request.params.repoId)
666
+ ?? queryString(request.query.repoId)
667
+ ?? queryString(body.repoId)
668
+ ?? options.defaultRepoId;
669
+ const checkoutId = queryString(request.params.checkoutId)
670
+ ?? queryString(request.query.checkoutId)
671
+ ?? queryString(body.checkoutId);
672
+ return resolveRepositorySelection({
673
+ repoId,
674
+ checkoutId,
675
+ explicitPath: repoId ? undefined : options.repoRoot,
676
+ controllerHome,
677
+ allowSoleRepository: true,
678
+ });
679
+ }
680
+
681
+ function requestRepositoryRoot(request: Request, options: LocalBridgeServerOptions, controllerHome: string): string {
682
+ return requestRepositorySelection(request, options, controllerHome).canonicalRoot;
683
+ }
684
+
685
+ function userFacingPluginStatus(plugin: LocalControllerSnapshot['assistantPlugins'][number]) {
686
+ const health = plugin.health;
687
+ const lifecycle = plugin.lifecycle;
688
+ const ready = plugin.enabled !== false && health.ready !== false && health.state !== 'error' && lifecycle.state !== 'error';
689
+ const needsAuthorization = [...(health.errors ?? []), ...(health.warnings ?? []), lifecycle.reason ?? '']
690
+ .some((entry) => /auth|token|credential|permission|scope|授权|登录/i.test(String(entry)));
691
+ const status = plugin.enabled === false ? 'disabled'
692
+ : ready ? 'ready'
693
+ : needsAuthorization ? 'authorization_required'
694
+ : health.state === 'error' || lifecycle.state === 'error' ? 'failed'
695
+ : 'needs_setup';
696
+ const nextStep = status === 'ready' ? '已连接,可用于任务执行'
697
+ : status === 'authorization_required' ? '需要完成授权后才能使用'
698
+ : status === 'disabled' ? '插件已禁用,可在高级配置中启用'
699
+ : '需要配置并测试连接';
700
+ return {
701
+ id: plugin.pluginId,
702
+ name: plugin.displayName || plugin.pluginId,
703
+ provider: plugin.provider,
704
+ status,
705
+ statusLabel: status === 'ready' ? '可用' : status === 'authorization_required' ? '待授权' : status === 'failed' ? '测试失败' : status === 'disabled' ? '已禁用' : '需配置',
706
+ enabled: plugin.enabled !== false,
707
+ actionCount: plugin.actions.length,
708
+ description: `${plugin.actions.length} 个可用动作`,
709
+ nextStep,
710
+ health: plugin.health,
711
+ lifecycle: plugin.lifecycle,
712
+ };
713
+ }
714
+
715
+ function userFacingAttentionItems(snapshot: LocalControllerSnapshot): UserFacingAttentionItem[] {
716
+ const items: UserFacingAttentionItem[] = [];
717
+ const seen = new Set<string>();
718
+ const push = (item: UserFacingAttentionItem): void => {
719
+ if (seen.has(item.id)) return;
720
+ seen.add(item.id);
721
+ items.push(item);
722
+ };
723
+ const queues = snapshot.completion.queues;
724
+ for (const entry of queues.needsHumanReview.slice(0, 12)) {
725
+ push({
726
+ id: `confirm:${entry.runId ?? `${entry.issueId}:${entry.taskId}`}`,
727
+ category: 'confirm_result',
728
+ categoryLabel: '任务待确认',
729
+ title: entry.title,
730
+ reason: entry.reason || '任务已经产出结果,需要你确认完成或打回修改。',
731
+ scope: '当前仓库',
732
+ icon: '✓',
733
+ primaryAction: action('view_advanced', '查看结果'),
734
+ refs: { issueId: entry.issueId, taskId: entry.taskId, runId: entry.runId },
735
+ });
736
+ }
737
+ for (const entry of queues.retryRequired.slice(0, 8)) {
738
+ push({
739
+ id: `retry:${entry.runId ?? `${entry.issueId}:${entry.taskId}`}`,
740
+ category: 'fix_failed_run',
741
+ categoryLabel: '失败需要处理',
742
+ title: entry.title,
743
+ reason: entry.reason || '最近一次执行失败,需要重新运行或打回修改。',
744
+ scope: '当前仓库',
745
+ icon: '!',
746
+ primaryAction: action('start_task', '重新发起'),
747
+ refs: { issueId: entry.issueId, taskId: entry.taskId, runId: entry.runId },
748
+ });
749
+ }
750
+ for (const entry of [...queues.noRunEvidence, ...queues.systemBlocked].slice(0, 8)) {
751
+ push({
752
+ id: `inspect:${entry.issueId}:${entry.taskId}`,
753
+ category: 'needs_inspection',
754
+ categoryLabel: '需要检查',
755
+ title: entry.title,
756
+ reason: entry.reason || '任务状态需要人工检查后再继续。',
757
+ scope: '当前仓库',
758
+ icon: '?',
759
+ primaryAction: action('view_advanced', '查看诊断'),
760
+ refs: { issueId: entry.issueId, taskId: entry.taskId, runId: entry.runId },
761
+ });
762
+ }
763
+ for (const plugin of snapshot.assistantPlugins.map(userFacingPluginStatus).filter((entry) => ['authorization_required', 'failed', 'needs_setup'].includes(entry.status)).slice(0, 8)) {
764
+ push({
765
+ id: `plugin:${plugin.id}`,
766
+ category: 'connect_plugin',
767
+ categoryLabel: '插件待授权',
768
+ title: `${plugin.name} ${plugin.statusLabel}`,
769
+ reason: plugin.nextStep,
770
+ scope: '能力中心',
771
+ icon: '✣',
772
+ primaryAction: action('configure_plugins', '去配置'),
773
+ refs: { pluginId: plugin.id },
774
+ });
775
+ }
776
+ const issues = Array.isArray(snapshot.board.issues) ? snapshot.board.issues : [];
777
+ for (const issue of issues.slice(0, 40)) {
778
+ const tasks = Array.isArray(issue.tasks) ? issue.tasks : [];
779
+ for (const task of tasks) {
780
+ const effectiveStatus = String((task as Record<string, unknown>).effectiveStatus ?? (task as Record<string, unknown>).status ?? '');
781
+ if (effectiveStatus !== 'changes_requested') continue;
782
+ push({
783
+ id: `changes:${issue.id}:${String((task as Record<string, unknown>).id ?? '')}`,
784
+ category: 'changes_requested',
785
+ categoryLabel: '已打回等待修改',
786
+ title: String((task as Record<string, unknown>).title ?? issue.title ?? '任务等待修改'),
787
+ reason: '该任务已经被打回,不应该继续显示成待审批。',
788
+ scope: String(issue.title ?? issue.id),
789
+ icon: '↩',
790
+ primaryAction: action('start_task', '继续修改'),
791
+ refs: { issueId: String(issue.id), taskId: String((task as Record<string, unknown>).id ?? '') },
792
+ });
793
+ }
794
+ }
795
+ return items.slice(0, 24);
796
+ }
797
+
798
+ function userFacingRecentTasks(snapshot: LocalControllerSnapshot) {
799
+ const queues = snapshot.decisionQueues;
800
+ const fromRunning = queues.runningNow.map((item) => {
801
+ const value = item as typeof item & Record<string, unknown>;
802
+ return {
803
+ id: String(value.id ?? value.runId ?? value.taskId ?? value.title),
804
+ title: String(value.title ?? '执行中的任务'),
805
+ summary: '助手正在处理,请等待下一步结果。',
806
+ status: 'running',
807
+ actor: 'G',
808
+ updatedAt: item.updatedAt,
809
+ };
810
+ });
811
+ const fromCompleted = queues.recentlyCompleted.map((item) => {
812
+ const value = item as typeof item & Record<string, unknown>;
813
+ return {
814
+ id: String(value.id ?? value.runId ?? value.taskId ?? value.title),
815
+ title: String(value.title ?? '已完成任务'),
816
+ summary: String(item.status) === 'cancelled' ? '任务已取消。' : '任务已经完成,可在历史中查看凭证。',
817
+ status: String(item.status) === 'cancelled' ? 'attention' : 'completed',
818
+ actor: 'G',
819
+ updatedAt: item.updatedAt,
820
+ };
821
+ });
822
+ const fromAttention = userFacingAttentionItems(snapshot).slice(0, 3).map((item) => ({
823
+ id: item.id,
824
+ title: item.title,
825
+ summary: item.reason,
826
+ status: 'attention',
827
+ actor: 'G',
828
+ updatedAt: undefined,
829
+ }));
830
+ return [...fromRunning, ...fromCompleted, ...fromAttention].slice(0, 8);
831
+ }
832
+
833
+ function buildUserControllerExperienceSnapshot(repoRoot: string, controllerHome = resolveControllerHome(), selectedRepoId?: string) {
834
+ const snapshot = cachedLocalControllerSnapshot(repoRoot);
835
+ const repositories = userFacingRepositories(repoRoot, controllerHome, selectedRepoId);
836
+ const currentRepository = repositories.find((entry) => entry.current) ?? repositories[0];
837
+ const plugins = snapshot.assistantPlugins.map(userFacingPluginStatus);
838
+ const operationalPlan = snapshot.operationalPlan;
839
+ const readyPlugins = plugins.filter((plugin) => plugin.status === 'ready');
840
+ const attentionItems = userFacingAttentionItems(snapshot);
841
+ const blocked = snapshot.recovery.overallState === 'blocked' || snapshot.recovery.overallState === 'unavailable';
842
+ const needsSetup = !currentRepository || currentRepository.status !== 'ready';
843
+ const readinessState = blocked ? 'blocked' : needsSetup ? 'needs_setup' : 'ready';
844
+ const recommendation = needsSetup
845
+ ? { title: '先选择或注册一个仓库', description: '仓库可用后才能执行任务。', action: action('manage_repositories', '管理仓库') }
846
+ : attentionItems.length > 0
847
+ ? { title: '先处理高优先级事项', description: `有 ${attentionItems.length} 项需要你处理,处理后助手即可继续推进。`, action: action('handle_attention', '处理待办') }
848
+ : readyPlugins.length < plugins.length
849
+ ? { title: '补全插件能力', description: '核心仓库能力可用,但部分助手能力还未连接。', action: action('configure_plugins', '配置插件') }
850
+ : { title: '创建一个新任务,让助手为你完成工作', description: '当前系统可用,你可以直接输入自然语言任务。', action: action('start_task', '执行一个任务') };
851
+ return {
852
+ schemaVersion: 1,
853
+ generatedAt: new Date().toISOString(),
854
+ readiness: {
855
+ state: readinessState,
856
+ label: readinessState === 'ready' ? '系统就绪' : readinessState === 'needs_setup' ? '需要设置' : '系统阻塞',
857
+ title: readinessState === 'ready' ? '系统可用' : readinessState === 'needs_setup' ? '需要完成设置' : '系统暂不可用',
858
+ description: currentRepository
859
+ ? `仓库 ${currentRepository.name} ${readinessState === 'ready' ? '已就绪,可接受任务与调度检查' : '需要处理后才能执行任务'}`
860
+ : '请先添加一个本地仓库。',
861
+ chips: [
862
+ { label: snapshot.execution.defaultMode === 'direct-edit' ? '直接执行模式' : '执行模式已设置', tone: 'green' },
863
+ { label: snapshot.runtime.registered ? '环境就绪' : '仓库未注册', tone: snapshot.runtime.registered ? 'green' : 'amber' },
864
+ { label: snapshot.connector.healthy === true ? '权限充足' : snapshot.connector.healthy === false ? '连接异常' : '连接待验证', tone: snapshot.connector.healthy === true ? 'green' : 'amber' },
865
+ { label: '安全防护已启用', tone: 'green' },
866
+ ],
867
+ primaryAction: recommendation.action,
868
+ },
869
+ currentRepository,
870
+ repositories,
871
+ pluginSummary: {
872
+ ready: readyPlugins.length,
873
+ total: plugins.length,
874
+ lines: plugins.slice(0, 5).map((plugin) => `${plugin.name} ${plugin.statusLabel}`),
875
+ },
876
+ operationalPlan,
877
+ operationalPlanSummary: {
878
+ status: operationalPlan.status,
879
+ validationPolicy: operationalPlan.validationStrategy.policy,
880
+ checks: operationalPlan.validationStrategy.checks,
881
+ dirty: operationalPlan.diffProjection.dirty,
882
+ recommendedWorker: operationalPlan.workerAbstraction.recommendedWorker,
883
+ panels: operationalPlan.guiInteraction.primaryPanels,
884
+ actions: operationalPlan.guiInteraction.primaryActions,
885
+ },
886
+ plugins,
887
+ attentionItems,
888
+ recommendation,
889
+ recentTasks: userFacingRecentTasks(snapshot),
890
+ };
891
+ }
892
+
893
+ export async function startLocalBridgeServer(
894
+ options: LocalBridgeServerOptions,
895
+ ): Promise<LocalBridgeServerHandle> {
896
+ const repositoryControllerHome = resolveRepoPreferredControllerHome(options.repoRoot, options.controllerHome);
897
+ const repository = registerRepository({ path: options.repoRoot, controllerHome: repositoryControllerHome });
898
+ ensureRepositoryRuntimeStorage(repository, repositoryControllerHome);
899
+ const repoRoot = repository.canonicalRoot;
900
+ const host = options.host ?? "127.0.0.1";
901
+ const requestedPort = options.port ?? 8766;
902
+ const controllerHome = repositoryControllerHome;
903
+ const mcpExposureContext = createControllerMcpToolContext({
904
+ repo: repoRoot,
905
+ controllerHome,
906
+ profile: "controller",
907
+ }) as MultiRepositoryMcpToolContext;
908
+ assertAllowedBindHost(host, options.allowLanMobileIntents === true);
909
+ reconcileAgentJobs(repoRoot);
910
+ reconcileLocalBridgeJobs(repoRoot);
911
+ const token = options.token ?? randomBytes(32).toString("base64url");
912
+ const app = express();
913
+ const streamClients = new Map<Response, { repoRoot: string; signature: string }>();
914
+ let streamIdleTicks = 0;
915
+ const sendStreamEvent = (response: Response, type: string): void => {
916
+ response.write(`event: ${type}\n`);
917
+ response.write(`data: ${JSON.stringify({ at: new Date().toISOString() })}\n\n`);
918
+ };
919
+ const streamInterval = setInterval(() => {
920
+ if (streamClients.size === 0) return;
921
+ let refreshed = false;
922
+ for (const [client, state] of streamClients) {
923
+ const next = controllerStateSignature(state.repoRoot);
924
+ if (next !== state.signature) {
925
+ streamClients.set(client, { ...state, signature: next });
926
+ sendStreamEvent(client, "refresh");
927
+ refreshed = true;
928
+ }
929
+ }
930
+ if (refreshed) {
931
+ streamIdleTicks = 0;
932
+ return;
933
+ }
934
+ streamIdleTicks += 1;
935
+ if (streamIdleTicks >= 8) {
936
+ streamIdleTicks = 0;
937
+ for (const client of streamClients.keys()) sendStreamEvent(client, "heartbeat");
938
+ }
939
+ }, 2_000);
940
+ streamInterval.unref();
941
+ const cookieName = "forge_local_token";
942
+ app.disable("x-powered-by");
943
+ app.use((request, response, next) => {
944
+ const requestHost = parsedRequestHost(request.headers.host);
945
+ const mobileIntentPath = request.path === "/mobile/intent" || request.path.startsWith("/mobile/");
946
+ if (!requestHost) {
947
+ response.status(403).json({ error: "invalid local controller host" });
948
+ return;
949
+ }
950
+ if (!isLoopbackHostname(requestHost.hostname) && !(options.allowLanMobileIntents === true && mobileIntentPath)) {
951
+ response.status(403).json({ error: "invalid local controller host" });
952
+ return;
953
+ }
954
+ const origin = request.headers.origin;
955
+ if (origin) {
956
+ try {
957
+ const parsedOrigin = new URL(origin);
958
+ if (parsedOrigin.host !== requestHost.host || (!isLoopbackHostname(parsedOrigin.hostname) && !(options.allowLanMobileIntents === true && mobileIntentPath))) {
959
+ response.status(403).json({ error: "invalid local controller origin" });
960
+ return;
961
+ }
962
+ } catch (_error) {
963
+ response.status(403).json({ error: "invalid local controller origin" });
964
+ return;
965
+ }
966
+ }
967
+ next();
968
+ });
969
+ app.use(express.json({
970
+ limit: "512kb",
971
+ verify: (request, _response, buffer) => {
972
+ (request as Request & { rawBody?: Buffer }).rawBody = Buffer.from(buffer);
973
+ },
974
+ }));
975
+
976
+ const requireToken = (
977
+ request: Request,
978
+ response: Response,
979
+ next: NextFunction,
980
+ ): void => {
981
+ const supplied =
982
+ request.header("x-forge-local-token") ?? cookieValue(request, cookieName);
983
+ if (supplied !== token) {
984
+ response.status(403).json({ error: "invalid local controller token" });
985
+ return;
986
+ }
987
+ next();
988
+ };
989
+
990
+ app.get("/", (_request, response) => {
991
+ response.setHeader("Cache-Control", "no-store, max-age=0");
992
+ response.setHeader("Pragma", "no-cache");
993
+ response.setHeader("Expires", "0");
994
+ response.setHeader("Referrer-Policy", "no-referrer");
995
+ response.setHeader(
996
+ "Set-Cookie",
997
+ `${cookieName}=${encodeURIComponent(token)}; Path=/api; HttpOnly; SameSite=Strict`,
998
+ );
999
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
1000
+ response.send(localBridgeDashboardHtml());
1001
+ });
1002
+ app.get("/health", (_request, response) => {
1003
+ const exposure = controllerExposureSnapshot(mcpExposureContext);
1004
+ const fingerprint = exposure.fingerprint;
1005
+ const runtime = readRuntimeGeneration(controllerHome);
1006
+ response.json({
1007
+ status: "ok",
1008
+ localOnly: true,
1009
+ mode: options.mode ?? "standalone",
1010
+ ...(options.slot ? { slot: options.slot } : {}),
1011
+ toolSurface: FORGE_TOOL_SURFACE,
1012
+ schemaVersion: FORGE_MCP_SCHEMA_VERSION,
1013
+ version: FORGE_VERSION,
1014
+ toolSurfaceFingerprint: fingerprint,
1015
+ runtimeToolSurfaceFingerprint: fingerprint,
1016
+ generation: runtime?.generation,
1017
+ source: runtime?.source,
1018
+ toolset: exposure.access.effectiveToolset,
1019
+ configuredAccessMode: exposure.access.configuredAccessMode,
1020
+ effectiveAccessMode: exposure.access.effectiveAccessMode,
1021
+ effectiveToolset: exposure.access.effectiveToolset,
1022
+ accessModeSource: exposure.access.source,
1023
+ accessModeLastAppliedAt: exposure.access.lastAppliedAt,
1024
+ exposureRevision: exposure.access.exposureRevision,
1025
+ toolCount: exposure.toolNames.length,
1026
+ });
1027
+ });
1028
+
1029
+
1030
+ app.post("/mobile/v1/monitor/snapshot", (request, response) => {
1031
+ try {
1032
+ const verified = verifyMobileIntentRequest(repoRoot, request);
1033
+ if (!mobileIntentHasScope(verified.principal.scopes, "monitor:read")) {
1034
+ throw new Error("MOBILE_INTENT_SCOPE_DENIED: monitor:read is required");
1035
+ }
1036
+ const selected = registerRepository({ path: repoRoot, controllerHome });
1037
+ response.setHeader("Cache-Control", "no-store");
1038
+ response.json({
1039
+ ...buildMobileMonitorSnapshot({
1040
+ controllerHome,
1041
+ repoId: selected.repoId,
1042
+ repoRoot: selected.canonicalRoot,
1043
+ repositoryName: selected.displayName,
1044
+ }),
1045
+ device: verified.principal.device,
1046
+ signatureVerified: verified.signatureVerified,
1047
+ });
1048
+ } catch (error) {
1049
+ const message = errorMessage(error);
1050
+ const status = message.includes("RATE_LIMITED") ? 429
1051
+ : message.includes("SCOPE_DENIED") ? 403
1052
+ : message.includes("TOKEN") || message.includes("SIGNATURE") || message.includes("REPLAY") || message.includes("TIMESTAMP") || message.includes("NONCE") || message.includes("DEVICE") ? 401
1053
+ : 400;
1054
+ response.status(status).json({ error: message });
1055
+ }
1056
+ });
1057
+
1058
+ app.get("/mobile/v1/monitor/events", (request, response) => {
1059
+ try {
1060
+ const verified = verifyMobileIntentRequest(repoRoot, request);
1061
+ if (!mobileIntentHasScope(verified.principal.scopes, "monitor:read")) {
1062
+ throw new Error("MOBILE_INTENT_SCOPE_DENIED: monitor:read is required");
1063
+ }
1064
+ response.status(200);
1065
+ response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
1066
+ response.setHeader("Cache-Control", "no-cache, no-transform");
1067
+ response.setHeader("Connection", "keep-alive");
1068
+ response.flushHeaders();
1069
+ streamClients.set(response, { repoRoot, signature: controllerStateSignature(repoRoot) });
1070
+ sendStreamEvent(response, "ready");
1071
+ request.on("close", () => streamClients.delete(response));
1072
+ } catch (error) {
1073
+ const message = errorMessage(error);
1074
+ const status = message.includes("RATE_LIMITED") ? 429
1075
+ : message.includes("SCOPE_DENIED") ? 403
1076
+ : message.includes("TOKEN") || message.includes("SIGNATURE") || message.includes("REPLAY") || message.includes("TIMESTAMP") || message.includes("NONCE") || message.includes("DEVICE") ? 401
1077
+ : 400;
1078
+ response.status(status).json({ error: message });
1079
+ }
1080
+ });
1081
+
1082
+ app.post("/mobile/intent", async (request, response) => {
1083
+ try {
1084
+ const verified = verifyMobileIntentRequest(repoRoot, request);
1085
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1086
+ ? request.body as Record<string, unknown>
1087
+ : {};
1088
+ const intent = queryString(body.intent) ?? "plugin_action";
1089
+ const repository = registerRepository({ path: repoRoot, controllerHome });
1090
+ if (intent === "list_plugins") {
1091
+ if (!mobileIntentHasScope(verified.principal.scopes, "plugins:read")) throw new Error("MOBILE_INTENT_SCOPE_DENIED: plugins:read is required");
1092
+ response.json({
1093
+ schemaVersion: 1,
1094
+ accepted: true,
1095
+ device: verified.principal.device,
1096
+ signatureVerified: verified.signatureVerified,
1097
+ plugins: listAssistantPluginManifests(controllerHome, repository).map((plugin) => ({
1098
+ pluginId: plugin.pluginId,
1099
+ displayName: plugin.displayName,
1100
+ enabled: plugin.enabled,
1101
+ lifecycle: plugin.lifecycle,
1102
+ health: plugin.health,
1103
+ actions: plugin.actions.map((action) => ({
1104
+ actionId: action.actionId,
1105
+ title: action.title,
1106
+ readOnly: action.readOnly,
1107
+ risk: action.risk,
1108
+ confirmation: action.confirmation,
1109
+ requiredConfirmationText: action.requiredConfirmationText,
1110
+ })),
1111
+ })),
1112
+ });
1113
+ return;
1114
+ }
1115
+ if (intent === "get_plugin") {
1116
+ if (!mobileIntentHasScope(verified.principal.scopes, "plugins:read")) throw new Error("MOBILE_INTENT_SCOPE_DENIED: plugins:read is required");
1117
+ const pluginId = queryString(body.pluginId);
1118
+ if (!pluginId) throw new Error("MOBILE_INTENT_PLUGIN_REQUIRED: pluginId is required");
1119
+ response.json({
1120
+ schemaVersion: 1,
1121
+ accepted: true,
1122
+ device: verified.principal.device,
1123
+ signatureVerified: verified.signatureVerified,
1124
+ plugin: getAssistantPluginManifest(controllerHome, repository, pluginId),
1125
+ });
1126
+ return;
1127
+ }
1128
+ if (intent === "poll_job") {
1129
+ if (!mobileIntentHasScope(verified.principal.scopes, "jobs:read")) throw new Error("MOBILE_INTENT_SCOPE_DENIED: jobs:read is required");
1130
+ const jobId = queryString(body.jobId);
1131
+ if (!jobId) throw new Error("MOBILE_INTENT_JOB_REQUIRED: jobId is required");
1132
+ const job = findExecutionJob(controllerHome, jobId)
1133
+ ?? (() => {
1134
+ const receipt = findPluginActionReceipt(controllerHome, jobId);
1135
+ return receipt ? compatibilityPluginJobFromReceipt(receipt) : undefined;
1136
+ })();
1137
+ if (!job) {
1138
+ response.status(404).json({ error: `Execution Job not found: ${jobId}` });
1139
+ return;
1140
+ }
1141
+ response.json({ schemaVersion: 1, accepted: true, device: verified.principal.device, signatureVerified: verified.signatureVerified, job });
1142
+ return;
1143
+ }
1144
+ if (intent !== "plugin_action") throw new Error(`MOBILE_INTENT_UNSUPPORTED: ${intent}`);
1145
+ const pluginId = queryString(body.pluginId);
1146
+ const actionId = queryString(body.actionId);
1147
+ if (!pluginId || !actionId) throw new Error("MOBILE_INTENT_ACTION_REQUIRED: pluginId and actionId are required");
1148
+ if (!mobileIntentHasScope(verified.principal.scopes, `plugin:${pluginId}:${actionId}`)) {
1149
+ throw new Error(`MOBILE_INTENT_SCOPE_DENIED: plugin:${pluginId}:${actionId} is required`);
1150
+ }
1151
+ const manifest = getAssistantPluginManifest(controllerHome, repository, pluginId);
1152
+ const action = manifest.actions.find((entry) => entry.actionId === actionId);
1153
+ if (!action) throw new Error(`PLUGIN_ACTION_NOT_FOUND: ${pluginId}/${actionId}`);
1154
+ if (action.confirmation !== "none" && body.confirmAuthorization !== true) {
1155
+ response.status(409).json({
1156
+ schemaVersion: 1,
1157
+ accepted: false,
1158
+ approvalRequired: true,
1159
+ device: verified.principal.device,
1160
+ plugin: { pluginId, displayName: manifest.displayName, enabled: manifest.enabled, lifecycle: manifest.lifecycle, health: manifest.health },
1161
+ action: {
1162
+ actionId: action.actionId,
1163
+ risk: action.risk,
1164
+ confirmation: action.confirmation,
1165
+ requiredConfirmationText: action.requiredConfirmationText,
1166
+ },
1167
+ message: action.confirmation === "strong_confirmation"
1168
+ ? `Repeat the request with confirmAuthorization=true and confirmationText=${action.requiredConfirmationText}`
1169
+ : "Repeat the request with confirmAuthorization=true after human approval.",
1170
+ });
1171
+ return;
1172
+ }
1173
+ const submitted = await submitAssistantPluginAction(controllerHome, repository, {
1174
+ pluginId,
1175
+ actionId,
1176
+ requestId: queryString(body.requestId) ?? `${verified.principal.deviceId}:${Date.now()}`,
1177
+ args: body.arguments && typeof body.arguments === "object" && !Array.isArray(body.arguments)
1178
+ ? body.arguments as Record<string, unknown>
1179
+ : {},
1180
+ timeoutMs: typeof body.timeoutMs === "number" ? body.timeoutMs : undefined,
1181
+ confirmAuthorization: body.confirmAuthorization === true,
1182
+ confirmationText: queryString(body.confirmationText),
1183
+ origin: { surface: "mobile-intent", actor: verified.principal.deviceId, correlationId: queryString(body.requestId) },
1184
+ });
1185
+ response.status(202).json({
1186
+ schemaVersion: 1,
1187
+ accepted: true,
1188
+ deduplicated: submitted.deduplicated,
1189
+ device: verified.principal.device,
1190
+ signatureVerified: verified.signatureVerified,
1191
+ pollAfterMs: 2_000,
1192
+ plugin: { pluginId: submitted.manifest.pluginId, displayName: submitted.manifest.displayName, lifecycle: submitted.manifest.lifecycle, health: submitted.manifest.health },
1193
+ action: { actionId: submitted.action.actionId, risk: submitted.action.risk, confirmation: submitted.action.confirmation, requiredConfirmationText: submitted.action.requiredConfirmationText },
1194
+ job: submitted.job,
1195
+ });
1196
+ } catch (error) {
1197
+ const message = errorMessage(error);
1198
+ const status = message.includes("RATE_LIMITED") ? 429
1199
+ : message.includes("SCOPE_DENIED") ? 403
1200
+ : message.includes("TOKEN") || message.includes("SIGNATURE") || message.includes("REPLAY") || message.includes("TIMESTAMP") || message.includes("NONCE") || message.includes("DEVICE") ? 401
1201
+ : 400;
1202
+ response.status(status).json({ error: message });
1203
+ }
1204
+ });
1205
+
1206
+ app.get("/oauth/google/callback", async (request, response) => {
1207
+ response.setHeader("Cache-Control", "no-store");
1208
+ response.setHeader("Referrer-Policy", "no-referrer");
1209
+ try {
1210
+ const result = await completeGoogleOAuthLogin(controllerHome, {
1211
+ state: queryString(request.query.state),
1212
+ code: queryString(request.query.code),
1213
+ error: queryString(request.query.error),
1214
+ errorDescription: queryString(request.query.error_description),
1215
+ });
1216
+ response.status(200).type("html").send(`<!doctype html><meta charset="utf-8"><title>Google authorization complete</title><h1>Google authorization complete</h1><p>${String(result.service)} is connected. You can close this window.</p>`);
1217
+ } catch (error) {
1218
+ response.status(400).type("html").send(`<!doctype html><meta charset="utf-8"><title>Google authorization failed</title><h1>Google authorization failed</h1><p>${errorMessage(error).replace(/[<>&]/g, "")}</p>`);
1219
+ }
1220
+ });
1221
+
1222
+ app.get("/oauth/resend/callback", async (request, response) => {
1223
+ response.setHeader("Cache-Control", "no-store");
1224
+ response.setHeader("Referrer-Policy", "no-referrer");
1225
+ try {
1226
+ await completeResendOAuthLogin(controllerHome, {
1227
+ state: queryString(request.query.state),
1228
+ code: queryString(request.query.code),
1229
+ error: queryString(request.query.error),
1230
+ errorDescription: queryString(request.query.error_description),
1231
+ });
1232
+ response.status(200).type("html").send('<!doctype html><meta charset="utf-8"><title>Resend authorization complete</title><h1>Resend authorization complete</h1><p>Forge is connected to Resend. You can close this window.</p>');
1233
+ } catch (error) {
1234
+ response.status(400).type("html").send(`<!doctype html><meta charset="utf-8"><title>Resend authorization failed</title><h1>Resend authorization failed</h1><p>${errorMessage(error).replace(/[<>&]/g, "")}</p>`);
1235
+ }
1236
+ });
1237
+
1238
+ app.use("/api", requireToken);
1239
+
1240
+ const consoleCtx = (request: Request) => {
1241
+ const repository = requestRepositorySelection(request, options, controllerHome);
1242
+ return { controllerHome, repository };
1243
+ };
1244
+
1245
+ app.get("/api/console/command-center", async (request, response) => {
1246
+ try {
1247
+ const ctx = consoleCtx(request);
1248
+ const repositories = userFacingRepositories(ctx.repository.canonicalRoot, controllerHome, ctx.repository.repoId)
1249
+ .map((entry) => {
1250
+ const record = loadRepositoryRegistry(controllerHome).repositories.find((item) => item.repoId === entry.id);
1251
+ if (!record) {
1252
+ return {
1253
+ id: entry.id,
1254
+ name: entry.name,
1255
+ path: entry.path,
1256
+ statusLabel: entry.statusLabel,
1257
+ tone: entry.current ? "green" as const : "blue" as const,
1258
+ current: entry.current,
1259
+ };
1260
+ }
1261
+ return mapRepositoryCard(record, entry.current);
1262
+ });
1263
+ response.json(await buildCommandCenter(ctx, repositories));
1264
+ } catch (error) {
1265
+ response.status(400).json({ error: errorMessage(error) });
1266
+ }
1267
+ });
1268
+
1269
+ app.get("/api/console/readiness", async (request, response) => {
1270
+ try {
1271
+ response.json(await buildSystemReadiness(consoleCtx(request)));
1272
+ } catch (error) {
1273
+ response.status(400).json({ error: errorMessage(error) });
1274
+ }
1275
+ });
1276
+
1277
+ /** Automation / Model & Tool Providers configuration center. */
1278
+ app.get("/api/console/automation-settings", (request, response) => {
1279
+ try {
1280
+ response.json(getAutomationSettings(consoleCtx(request)));
1281
+ } catch (error) {
1282
+ response.status(400).json({ error: errorMessage(error) });
1283
+ }
1284
+ });
1285
+ app.get("/api/console/provider-config", (request, response) => {
1286
+ try {
1287
+ response.json({ config: consoleProviderConfigGet(consoleCtx(request)), redacted: true });
1288
+ } catch (error) {
1289
+ response.status(400).json({ error: errorMessage(error) });
1290
+ }
1291
+ });
1292
+ app.post("/api/console/provider-config", (request, response) => {
1293
+ try {
1294
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1295
+ ? request.body as Record<string, unknown>
1296
+ : {};
1297
+ response.json({ config: consoleProviderConfigUpdate(consoleCtx(request), body), redacted: true });
1298
+ } catch (error) {
1299
+ response.status(400).json({ error: errorMessage(error) });
1300
+ }
1301
+ });
1302
+ app.post("/api/console/providers/:providerId/enable", (request, response) => {
1303
+ try {
1304
+ response.json({ config: consoleProviderEnable(consoleCtx(request), String(request.params.providerId ?? "")), redacted: true });
1305
+ } catch (error) {
1306
+ response.status(400).json({ error: errorMessage(error) });
1307
+ }
1308
+ });
1309
+ app.post("/api/console/providers/:providerId/disable", (request, response) => {
1310
+ try {
1311
+ response.json({ config: consoleProviderDisable(consoleCtx(request), String(request.params.providerId ?? "")), redacted: true });
1312
+ } catch (error) {
1313
+ response.status(400).json({ error: errorMessage(error) });
1314
+ }
1315
+ });
1316
+ app.post("/api/console/providers/:providerId/priority", (request, response) => {
1317
+ try {
1318
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1319
+ ? request.body as Record<string, unknown>
1320
+ : {};
1321
+ const direction = body.direction === "up" || body.direction === "down"
1322
+ ? body.direction
1323
+ : typeof body.priority === "number"
1324
+ ? body.priority
1325
+ : "up";
1326
+ response.json({
1327
+ config: consoleProviderPriority(consoleCtx(request), String(request.params.providerId ?? ""), direction as "up" | "down" | number),
1328
+ redacted: true,
1329
+ });
1330
+ } catch (error) {
1331
+ response.status(400).json({ error: errorMessage(error) });
1332
+ }
1333
+ });
1334
+ app.post("/api/console/providers/health", (request, response) => {
1335
+ try {
1336
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1337
+ ? request.body as Record<string, unknown>
1338
+ : {};
1339
+ const providerId = typeof body.providerId === "string" ? body.providerId : typeof body.provider_id === "string" ? body.provider_id : undefined;
1340
+ response.json(consoleProviderHealth(consoleCtx(request), providerId));
1341
+ } catch (error) {
1342
+ response.status(400).json({ error: errorMessage(error) });
1343
+ }
1344
+ });
1345
+ app.get("/api/console/providers/credentials", (request, response) => {
1346
+ try {
1347
+ response.json({ credentials: consoleProviderCredentials(consoleCtx(request)), redacted: true });
1348
+ } catch (error) {
1349
+ response.status(400).json({ error: errorMessage(error) });
1350
+ }
1351
+ });
1352
+ app.post("/api/console/providers/reset", (request, response) => {
1353
+ try {
1354
+ response.json({ config: consoleProviderReset(consoleCtx(request)), redacted: true });
1355
+ } catch (error) {
1356
+ response.status(400).json({ error: errorMessage(error) });
1357
+ }
1358
+ });
1359
+ app.get("/api/console/providers/:providerId/api-settings", (request, response) => {
1360
+ try {
1361
+ response.json(consoleProviderApiSettingsGet(consoleCtx(request), String(request.params.providerId ?? "")));
1362
+ } catch (error) {
1363
+ response.status(400).json({ error: errorMessage(error) });
1364
+ }
1365
+ });
1366
+ app.post("/api/console/providers/:providerId/api-settings", (request, response) => {
1367
+ try {
1368
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1369
+ ? request.body as Record<string, unknown>
1370
+ : {};
1371
+ response.json(consoleProviderApiSettingsUpdate(
1372
+ consoleCtx(request),
1373
+ String(request.params.providerId ?? ""),
1374
+ body,
1375
+ ));
1376
+ } catch (error) {
1377
+ response.status(400).json({ error: errorMessage(error) });
1378
+ }
1379
+ });
1380
+ app.get("/api/console/local-tools", (request, response) => {
1381
+ try {
1382
+ response.json({ tools: consoleLocalToolList(consoleCtx(request)), redacted: true });
1383
+ } catch (error) {
1384
+ response.status(400).json({ error: errorMessage(error) });
1385
+ }
1386
+ });
1387
+ app.post("/api/console/local-tools/:toolId/enable", (request, response) => {
1388
+ try {
1389
+ response.json({ config: consoleLocalToolEnable(consoleCtx(request), String(request.params.toolId ?? "")), redacted: true });
1390
+ } catch (error) {
1391
+ response.status(400).json({ error: errorMessage(error) });
1392
+ }
1393
+ });
1394
+ app.post("/api/console/local-tools/:toolId/disable", (request, response) => {
1395
+ try {
1396
+ response.json({ config: consoleLocalToolDisable(consoleCtx(request), String(request.params.toolId ?? "")), redacted: true });
1397
+ } catch (error) {
1398
+ response.status(400).json({ error: errorMessage(error) });
1399
+ }
1400
+ });
1401
+ app.post("/api/console/local-tools/health", (request, response) => {
1402
+ try {
1403
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1404
+ ? request.body as Record<string, unknown>
1405
+ : {};
1406
+ const toolId = typeof body.toolId === "string" ? body.toolId : typeof body.tool_id === "string" ? body.tool_id : undefined;
1407
+ response.json(consoleLocalToolHealth(consoleCtx(request), toolId));
1408
+ } catch (error) {
1409
+ response.status(400).json({ error: errorMessage(error) });
1410
+ }
1411
+ });
1412
+ app.get("/api/console/local-tool-config", (request, response) => {
1413
+ try {
1414
+ response.json({ config: consoleLocalToolConfigGet(consoleCtx(request)), redacted: true });
1415
+ } catch (error) {
1416
+ response.status(400).json({ error: errorMessage(error) });
1417
+ }
1418
+ });
1419
+ app.post("/api/console/local-tool-config", (request, response) => {
1420
+ try {
1421
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1422
+ ? request.body as Record<string, unknown>
1423
+ : {};
1424
+ response.json({ config: consoleLocalToolConfigUpdate(consoleCtx(request), body), redacted: true });
1425
+ } catch (error) {
1426
+ response.status(400).json({ error: errorMessage(error) });
1427
+ }
1428
+ });
1429
+ app.get("/api/console/executor-routing", (request, response) => {
1430
+ try {
1431
+ response.json({ config: consoleRoutingGet(consoleCtx(request)), redacted: true });
1432
+ } catch (error) {
1433
+ response.status(400).json({ error: errorMessage(error) });
1434
+ }
1435
+ });
1436
+ app.post("/api/console/executor-routing/reset", (request, response) => {
1437
+ try {
1438
+ response.json({ config: consoleRoutingReset(consoleCtx(request)), redacted: true });
1439
+ } catch (error) {
1440
+ response.status(400).json({ error: errorMessage(error) });
1441
+ }
1442
+ });
1443
+ app.post("/api/console/executor-routing", (request, response) => {
1444
+ try {
1445
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1446
+ ? request.body as Record<string, unknown>
1447
+ : {};
1448
+ response.json({ ...consoleRoutingUpdate(consoleCtx(request), body), redacted: true });
1449
+ } catch (error) {
1450
+ response.status(400).json({ error: errorMessage(error) });
1451
+ }
1452
+ });
1453
+ app.post("/api/console/executor-route-preview", (request, response) => {
1454
+ try {
1455
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1456
+ ? request.body as Record<string, unknown>
1457
+ : {};
1458
+ response.json(consoleRoutePreview(consoleCtx(request), body));
1459
+ } catch (error) {
1460
+ response.status(400).json({ error: errorMessage(error) });
1461
+ }
1462
+ });
1463
+ app.get("/api/console/goal-loop-policy", (request, response) => {
1464
+ try {
1465
+ response.json({ policy: consoleGoalLoopPolicyGet(consoleCtx(request)), redacted: true });
1466
+ } catch (error) {
1467
+ response.status(400).json({ error: errorMessage(error) });
1468
+ }
1469
+ });
1470
+ app.post("/api/console/goal-loop-policy", (request, response) => {
1471
+ try {
1472
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1473
+ ? request.body as Record<string, unknown>
1474
+ : {};
1475
+ response.json({ policy: consoleGoalLoopPolicyUpdate(consoleCtx(request), body), redacted: true });
1476
+ } catch (error) {
1477
+ response.status(400).json({ error: errorMessage(error) });
1478
+ }
1479
+ });
1480
+
1481
+ /** User-facing plugin/capability catalog for the console. */
1482
+ app.get("/api/console/plugins", (request, response) => {
1483
+ try {
1484
+ const ctx = consoleCtx(request);
1485
+ const plugins = listConsolePlugins(ctx);
1486
+ response.json({
1487
+ schemaVersion: 1,
1488
+ generatedAt: new Date().toISOString(),
1489
+ summary: buildPluginSummary(plugins),
1490
+ plugins,
1491
+ });
1492
+ } catch (error) {
1493
+ response.status(400).json({ error: errorMessage(error) });
1494
+ }
1495
+ });
1496
+
1497
+ app.get("/api/console/plugins/:pluginId", (request, response) => {
1498
+ try {
1499
+ const plugin = getConsolePlugin(consoleCtx(request), request.params.pluginId);
1500
+ if (!plugin) {
1501
+ response.status(404).json({ error: `PLUGIN_NOT_FOUND: ${request.params.pluginId}` });
1502
+ return;
1503
+ }
1504
+ response.json({ schemaVersion: 1, plugin });
1505
+ } catch (error) {
1506
+ response.status(400).json({ error: errorMessage(error) });
1507
+ }
1508
+ });
1509
+
1510
+ /** Local MCP tool-surface self-test. Does not invent ChatGPT connector tool names. */
1511
+ app.get("/api/console/connector/status", async (request, response) => {
1512
+ try {
1513
+ const report = await evaluateConsoleConnectorFreshness(consoleCtx(request));
1514
+ response.json({
1515
+ responseSchemaVersion: 1,
1516
+ generatedAt: new Date().toISOString(),
1517
+ ...report,
1518
+ });
1519
+ } catch (error) {
1520
+ response.status(400).json({ error: errorMessage(error) });
1521
+ }
1522
+ });
1523
+
1524
+ /**
1525
+ * Optional precise check when ChatGPT connector tool names are known.
1526
+ * Body: { connector_tool_names?: string[] }
1527
+ */
1528
+ app.post("/api/console/connector/check", async (request, response) => {
1529
+ try {
1530
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1531
+ ? request.body as Record<string, unknown>
1532
+ : {};
1533
+ const connectorToolNames = Array.isArray(body.connector_tool_names)
1534
+ ? body.connector_tool_names.map(String)
1535
+ : Array.isArray(body.connectorToolNames)
1536
+ ? body.connectorToolNames.map(String)
1537
+ : undefined;
1538
+ const report = await evaluateConsoleConnectorFreshness(consoleCtx(request), {
1539
+ connectorToolNames,
1540
+ });
1541
+ response.json({
1542
+ responseSchemaVersion: 1,
1543
+ generatedAt: new Date().toISOString(),
1544
+ connectorToolNamesProvided: connectorToolNames !== undefined,
1545
+ ...report,
1546
+ });
1547
+ } catch (error) {
1548
+ response.status(400).json({ error: errorMessage(error) });
1549
+ }
1550
+ });
1551
+
1552
+ app.get("/api/console/access-policy", (request, response) => {
1553
+ try {
1554
+ response.json(getConsoleAccessPolicy(consoleCtx(request)));
1555
+ } catch (error) {
1556
+ response.status(400).json({ error: errorMessage(error) });
1557
+ }
1558
+ });
1559
+
1560
+ app.post("/api/console/access-policy", (request, response) => {
1561
+ try {
1562
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1563
+ ? request.body as Record<string, unknown>
1564
+ : {};
1565
+ response.json(setConsoleAccessPolicy(consoleCtx(request), {
1566
+ mode: body.mode,
1567
+ confirmAuthorization: body.confirmAuthorization === true,
1568
+ confirmationText: queryString(body.confirmationText),
1569
+ }));
1570
+ } catch (error) {
1571
+ response.status(400).json({ error: errorMessage(error) });
1572
+ }
1573
+ });
1574
+
1575
+ app.post("/api/console/mode-preview", (request, response) => {
1576
+ try {
1577
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1578
+ ? request.body as Record<string, unknown>
1579
+ : {};
1580
+ const modePreview = previewExecutionMode({
1581
+ objective: queryString(body.objective) ?? "",
1582
+ expectedFiles: typeof body.expectedFiles === "number" ? body.expectedFiles : undefined,
1583
+ expectedChangedLines: typeof body.expectedChangedLines === "number" ? body.expectedChangedLines : undefined,
1584
+ scopeClear: body.scopeClear !== false,
1585
+ requiresInvestigation: body.requiresInvestigation === true,
1586
+ requiresLongRunningChecks: body.requiresLongRunningChecks === true,
1587
+ requiresWorker: body.requiresWorker === true,
1588
+ requiresApproval: body.requiresApproval === true,
1589
+ destructive: body.destructive === true,
1590
+ });
1591
+ response.json({ modePreview });
1592
+ } catch (error) {
1593
+ response.status(400).json({ error: errorMessage(error) });
1594
+ }
1595
+ });
1596
+
1597
+ app.get("/api/console/inbox", (request, response) => {
1598
+ try {
1599
+ const status = queryString(request.query.status) === "all" ? "all"
1600
+ : queryString(request.query.status) === "active" ? "active"
1601
+ : "pending";
1602
+ response.json({ items: listConsoleHandoffs(consoleCtx(request), status) });
1603
+ } catch (error) {
1604
+ response.status(400).json({ error: errorMessage(error) });
1605
+ }
1606
+ });
1607
+
1608
+ app.get("/api/console/inbox/:handoffId", (request, response) => {
1609
+ try {
1610
+ const item = getConsoleHandoff(consoleCtx(request), String(request.params.handoffId ?? ""));
1611
+ if (!item) {
1612
+ response.status(404).json({ error: "handoff not found" });
1613
+ return;
1614
+ }
1615
+ response.json({ item });
1616
+ } catch (error) {
1617
+ response.status(400).json({ error: errorMessage(error) });
1618
+ }
1619
+ });
1620
+
1621
+ app.post("/api/console/inbox/:handoffId/ack", (request, response) => {
1622
+ try {
1623
+ response.json({ item: ackConsoleHandoff(consoleCtx(request), String(request.params.handoffId ?? "")) });
1624
+ } catch (error) {
1625
+ response.status(400).json({ error: errorMessage(error) });
1626
+ }
1627
+ });
1628
+
1629
+ app.post("/api/console/inbox/:handoffId/approve", (request, response) => {
1630
+ try {
1631
+ const result = approveConsoleHandoff(
1632
+ consoleCtx(request),
1633
+ String(request.params.handoffId ?? ""),
1634
+ "user",
1635
+ );
1636
+ const feedback = toConsoleOperationFeedback(result.actionResult);
1637
+ response.status(result.continued ? 200 : 409).json({
1638
+ ...result,
1639
+ phase: feedback.phase,
1640
+ statusLabel: feedback.statusLabel,
1641
+ feedback,
1642
+ });
1643
+ } catch (error) {
1644
+ response.status(400).json({ error: errorMessage(error) });
1645
+ }
1646
+ });
1647
+
1648
+ app.post("/api/console/inbox/:handoffId/resolve", (request, response) => {
1649
+ try {
1650
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1651
+ ? request.body as Record<string, unknown>
1652
+ : {};
1653
+ response.json({
1654
+ item: resolveConsoleHandoff(
1655
+ consoleCtx(request),
1656
+ String(request.params.handoffId ?? ""),
1657
+ queryString(body.decision) ?? "resolved",
1658
+ queryString(body.resolver) ?? "user",
1659
+ ),
1660
+ });
1661
+ } catch (error) {
1662
+ response.status(400).json({ error: errorMessage(error) });
1663
+ }
1664
+ });
1665
+
1666
+ app.post("/api/console/inbox/:handoffId/dismiss", (request, response) => {
1667
+ try {
1668
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1669
+ ? request.body as Record<string, unknown>
1670
+ : {};
1671
+ response.json({
1672
+ item: dismissConsoleHandoff(
1673
+ consoleCtx(request),
1674
+ String(request.params.handoffId ?? ""),
1675
+ queryString(body.decision) ?? "dismissed",
1676
+ queryString(body.resolver) ?? "user",
1677
+ ),
1678
+ });
1679
+ } catch (error) {
1680
+ response.status(400).json({ error: errorMessage(error) });
1681
+ }
1682
+ });
1683
+
1684
+ app.get("/api/console/work", (request, response) => {
1685
+ try {
1686
+ const status = queryString(request.query.status) === "all" ? "all" : "active";
1687
+ response.json({ items: listConsoleWork(consoleCtx(request), status) });
1688
+ } catch (error) {
1689
+ response.status(400).json({ error: errorMessage(error) });
1690
+ }
1691
+ });
1692
+
1693
+ app.get("/api/console/work/:workId", (request, response) => {
1694
+ try {
1695
+ const work = getConsoleWork(consoleCtx(request), String(request.params.workId ?? ""));
1696
+ if (!work) {
1697
+ response.status(404).json({ error: "work not found" });
1698
+ return;
1699
+ }
1700
+ response.json({ work });
1701
+ } catch (error) {
1702
+ response.status(400).json({ error: errorMessage(error) });
1703
+ }
1704
+ });
1705
+
1706
+ app.post("/api/console/work/start", (request, response) => {
1707
+ try {
1708
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1709
+ ? request.body as Record<string, unknown>
1710
+ : {};
1711
+ const result = startConsoleWork(consoleCtx(request), {
1712
+ objective: queryString(body.objective) ?? "",
1713
+ acceptanceCriteria: Array.isArray(body.acceptanceCriteria) ? body.acceptanceCriteria.map(String) : undefined,
1714
+ allowedPaths: Array.isArray(body.allowedPaths) ? body.allowedPaths.map(String) : undefined,
1715
+ forbiddenPaths: Array.isArray(body.forbiddenPaths) ? body.forbiddenPaths.map(String) : undefined,
1716
+ expectedFiles: typeof body.expectedFiles === "number" ? body.expectedFiles : undefined,
1717
+ expectedChangedLines: typeof body.expectedChangedLines === "number" ? body.expectedChangedLines : undefined,
1718
+ scopeClear: body.scopeClear !== false,
1719
+ requiresInvestigation: body.requiresInvestigation === true,
1720
+ requiresLongRunningChecks: body.requiresLongRunningChecks === true,
1721
+ requiresWorker: body.requiresWorker === true,
1722
+ requiresApproval: body.requiresApproval === true,
1723
+ destructive: body.destructive === true,
1724
+ accessMode: body.accessMode === 'full_access' ? 'full_access' : body.accessMode === 'request' ? 'request' : undefined,
1725
+ approvalConfirmed: body.approvalConfirmed === true,
1726
+ forceMode: body.forceMode === "direct_control" || body.forceMode === "goal_workloop" || body.forceMode === "handoff_only"
1727
+ ? body.forceMode
1728
+ : undefined,
1729
+ checkIds: Array.isArray(body.checkIds) ? body.checkIds.map(String) : undefined,
1730
+ });
1731
+ const feedback = toConsoleOperationFeedback(result);
1732
+ response.status(result.status === "blocked" || result.status === "failed" ? 409 : 200).json({
1733
+ ...result,
1734
+ ...feedback,
1735
+ feedback,
1736
+ });
1737
+ } catch (error) {
1738
+ response.status(400).json({ error: errorMessage(error) });
1739
+ }
1740
+ });
1741
+
1742
+ const withFeedback = (result: Parameters<typeof toConsoleOperationFeedback>[0]) => {
1743
+ const feedback = toConsoleOperationFeedback(result);
1744
+ return { ...result, ...feedback, feedback };
1745
+ };
1746
+
1747
+ app.post("/api/console/work/continue", (request, response) => {
1748
+ try {
1749
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1750
+ ? request.body as Record<string, unknown>
1751
+ : {};
1752
+ const result = continueConsoleWork(consoleCtx(request), queryString(body.workId) ?? "", queryString(body.note));
1753
+ response.status(result.status === "not_found" ? 404 : result.status === "blocked" || result.status === "failed" ? 409 : 200).json(withFeedback(result));
1754
+ } catch (error) {
1755
+ response.status(400).json({ error: errorMessage(error) });
1756
+ }
1757
+ });
1758
+
1759
+ app.post("/api/console/work/verify", (request, response) => {
1760
+ try {
1761
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1762
+ ? request.body as Record<string, unknown>
1763
+ : {};
1764
+ const result = verifyConsoleWork(consoleCtx(request), {
1765
+ workId: queryString(body.workId) ?? "",
1766
+ checkId: queryString(body.checkId),
1767
+ simulate: body.simulate === true,
1768
+ checkFailed: body.checkFailed === true,
1769
+ infrastructureFailed: body.infrastructureFailed === true,
1770
+ });
1771
+ response.status(result.status === "not_found" ? 404 : result.status === "failed" ? 409 : 200).json(withFeedback(result));
1772
+ } catch (error) {
1773
+ response.status(400).json({ error: errorMessage(error) });
1774
+ }
1775
+ });
1776
+
1777
+ app.post("/api/console/work/finalize", (request, response) => {
1778
+ try {
1779
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1780
+ ? request.body as Record<string, unknown>
1781
+ : {};
1782
+ const result = finalizeConsoleWork(consoleCtx(request), queryString(body.workId) ?? "");
1783
+ response.status(result.status === "not_found" ? 404 : result.status === "failed" || result.status === "blocked" ? 409 : 200).json(withFeedback(result));
1784
+ } catch (error) {
1785
+ response.status(400).json({ error: errorMessage(error) });
1786
+ }
1787
+ });
1788
+
1789
+ app.post("/api/console/work/stop", (request, response) => {
1790
+ try {
1791
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1792
+ ? request.body as Record<string, unknown>
1793
+ : {};
1794
+ const result = stopConsoleWork(consoleCtx(request), queryString(body.workId) ?? "", queryString(body.reason));
1795
+ response.status(result.status === "not_found" ? 404 : 200).json(withFeedback(result));
1796
+ } catch (error) {
1797
+ response.status(400).json({ error: errorMessage(error) });
1798
+ }
1799
+ });
1800
+
1801
+ app.post("/api/console/work/delegate", (request, response) => {
1802
+ try {
1803
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1804
+ ? request.body as Record<string, unknown>
1805
+ : {};
1806
+ const target = queryString(body.target);
1807
+ const result = delegateConsoleWork(consoleCtx(request), {
1808
+ workId: queryString(body.workId),
1809
+ target: target === "grok" || target === "claude" || target === "codex" ? target : "codex",
1810
+ objective: queryString(body.objective),
1811
+ available: typeof body.available === "boolean" ? body.available : undefined,
1812
+ });
1813
+ response.status(result.status === "blocked" ? 409 : 200).json(withFeedback(result));
1814
+ } catch (error) {
1815
+ response.status(400).json({ error: errorMessage(error) });
1816
+ }
1817
+ });
1818
+
1819
+ app.post("/api/console/repair", (request, response) => {
1820
+ try {
1821
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1822
+ ? request.body as Record<string, unknown>
1823
+ : {};
1824
+ const operation = queryString(body.operation);
1825
+ const result = repairConsole(consoleCtx(request), {
1826
+ operation: operation === "repair" || operation === "verify" || operation === "handoff" || operation === "diagnose"
1827
+ ? operation
1828
+ : "diagnose",
1829
+ dryRun: body.dryRun === undefined ? true : body.dryRun === true,
1830
+ approvalConfirmed: body.approvalConfirmed === true,
1831
+ destructive: body.destructive === true,
1832
+ workId: queryString(body.workId),
1833
+ });
1834
+ response.status(result.status === "approval_required" || result.status === "blocked" ? 409 : 200).json({
1835
+ ...result,
1836
+ phase: result.status === "ok" ? "succeeded" : result.status === "approval_required" ? "needs_attention" : "failed",
1837
+ statusLabel: result.status === "ok" ? "已完成" : result.status === "approval_required" ? "需要确认" : "需要处理",
1838
+ terminal: true,
1839
+ });
1840
+ } catch (error) {
1841
+ response.status(400).json({
1842
+ error: errorMessage(error),
1843
+ errorClass: "unknown_failure",
1844
+ summary: errorMessage(error) || "操作失败,但未提供详细错误。",
1845
+ phase: "failed",
1846
+ statusLabel: "失败",
1847
+ });
1848
+ }
1849
+ });
1850
+
1851
+ app.post("/api/console/edit/apply", (request, response) => {
1852
+ try {
1853
+ const body = request.body && typeof request.body === "object" && !Array.isArray(request.body)
1854
+ ? request.body as Record<string, unknown>
1855
+ : {};
1856
+ const applied = applyConsoleSafePatch(consoleCtx(request), {
1857
+ operations: body.operations,
1858
+ purpose: queryString(body.purpose),
1859
+ allowedPaths: Array.isArray(body.allowedPaths) ? body.allowedPaths.map(String) : undefined,
1860
+ sessionId: queryString(body.sessionId),
1861
+ });
1862
+ response.status(applied.phase === "failed" ? 409 : 200).json(applied);
1863
+ } catch (error) {
1864
+ response.status(400).json({
1865
+ error: errorMessage(error),
1866
+ errorClass: "unknown_failure",
1867
+ summary: errorMessage(error) || "同步修改失败。",
1868
+ phase: "failed",
1869
+ statusLabel: "失败",
1870
+ terminal: true,
1871
+ });
1872
+ }
1873
+ });
1874
+
1875
+ app.get("/api/console/advanced", async (request, response) => {
1876
+ try {
1877
+ const ctx = consoleCtx(request);
1878
+ const raw = cachedLocalControllerSnapshot(ctx.repository.canonicalRoot);
1879
+ response.json(await buildAdvancedDiagnosticsEnvelope(raw, ctx));
1880
+ } catch (error) {
1881
+ response.status(400).json({ error: errorMessage(error) });
1882
+ }
1883
+ });
1884
+
1885
+ app.get("/api/snapshot", (request, response) => {
1886
+ try {
1887
+ response.json(cachedLocalControllerSnapshot(requestRepositoryRoot(request, options, controllerHome)));
1888
+ } catch (error) {
1889
+ response.status(400).json({ error: errorMessage(error) });
1890
+ }
1891
+ });
1892
+ app.get("/api/user-snapshot", (request, response) => {
1893
+ try {
1894
+ const repository = requestRepositorySelection(request, options, controllerHome);
1895
+ response.json(buildUserControllerExperienceSnapshot(repository.canonicalRoot, controllerHome, repository.repoId));
1896
+ } catch (error) {
1897
+ response.status(400).json({ error: errorMessage(error) });
1898
+ }
1899
+ });
1900
+
1901
+ app.get("/api/repositories", (request, response) => {
1902
+ try {
1903
+ const selectedRepoId = queryString(request.query.repoId) ?? options.defaultRepoId;
1904
+ response.json({ repositories: userFacingRepositories(repoRoot, controllerHome, selectedRepoId) });
1905
+ } catch (error) {
1906
+ response.status(400).json({ error: errorMessage(error) });
1907
+ }
1908
+ });
1909
+
1910
+ app.get("/api/repositories/:repoId/snapshot", (request, response) => {
1911
+ try {
1912
+ response.json(cachedLocalControllerSnapshot(requestRepositoryRoot(request, options, controllerHome)));
1913
+ } catch (error) {
1914
+ response.status(400).json({ error: errorMessage(error) });
1915
+ }
1916
+ });
1917
+
1918
+ app.get("/api/repositories/:repoId/user-snapshot", (request, response) => {
1919
+ try {
1920
+ const repository = requestRepositorySelection(request, options, controllerHome);
1921
+ response.json(buildUserControllerExperienceSnapshot(repository.canonicalRoot, controllerHome, repository.repoId));
1922
+ } catch (error) {
1923
+ response.status(400).json({ error: errorMessage(error) });
1924
+ }
1925
+ });
1926
+
1927
+ app.post("/api/repositories/register", (request, response) => {
1928
+ try {
1929
+ const path = queryString(request.body?.path);
1930
+ if (!path) throw new Error("REPOSITORY_PATH_REQUIRED");
1931
+ const repository = registerRepository({
1932
+ path,
1933
+ controllerHome,
1934
+ displayName: queryString(request.body?.displayName),
1935
+ remoteUrl: queryString(request.body?.remoteUrl),
1936
+ defaultBranch: queryString(request.body?.defaultBranch),
1937
+ });
1938
+ localSnapshotCache.delete(repository.canonicalRoot);
1939
+ response.status(201).json({ repository, userSnapshot: buildUserControllerExperienceSnapshot(repository.canonicalRoot, controllerHome, repository.repoId) });
1940
+ } catch (error) {
1941
+ response.status(400).json({ error: errorMessage(error) });
1942
+ }
1943
+ });
1944
+
1945
+ app.post("/api/repositories/:repoId/remove", (request, response) => {
1946
+ try {
1947
+ const repoId = queryString(request.params.repoId);
1948
+ if (!repoId) throw new Error("REPOSITORY_ID_REQUIRED");
1949
+ const repository = removeRepository(repoId, controllerHome);
1950
+ localSnapshotCache.delete(repository.canonicalRoot);
1951
+ for (const checkout of repository.checkouts ?? []) {
1952
+ if (checkout.canonicalRoot) localSnapshotCache.delete(checkout.canonicalRoot);
1953
+ }
1954
+ const remaining = userFacingRepositories(repoRoot, controllerHome);
1955
+ response.json({
1956
+ repository,
1957
+ repositories: remaining,
1958
+ summary: `已删除仓库注册:${repository.displayName || repository.repoId}`,
1959
+ });
1960
+ } catch (error) {
1961
+ const message = errorMessage(error);
1962
+ if (message.startsWith("REPOSITORY_SELF_PROTECTED")) {
1963
+ response.status(400).json({
1964
+ error: message,
1965
+ errorMessage: "不能删除当前进程所在仓库的注册(自我保护)",
1966
+ });
1967
+ return;
1968
+ }
1969
+ response.status(400).json({ error: message });
1970
+ }
1971
+ });
1972
+ app.get("/api/completion/backlog", (request, response) => {
1973
+ try {
1974
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
1975
+ const limit = Number(request.query.limit);
1976
+ response.json(inspectCompletionBacklog(repoRoot, { limit: Number.isFinite(limit) ? limit : 100 }));
1977
+ } catch (error) {
1978
+ response.status(400).json({ error: errorMessage(error) });
1979
+ }
1980
+ });
1981
+ app.get("/api/completion/queues", (request, response) => {
1982
+ try {
1983
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
1984
+ const limit = Number(request.query.limit);
1985
+ response.json(completionDecisionQueues(repoRoot, { limit: Number.isFinite(limit) ? limit : 100 }));
1986
+ } catch (error) {
1987
+ response.status(400).json({ error: errorMessage(error) });
1988
+ }
1989
+ });
1990
+ app.post("/api/completion/finish-ready-runs", (request, response) => {
1991
+ try {
1992
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
1993
+ const result = finishCompletionBacklog(repoRoot, {
1994
+ dryRun: request.body?.apply !== true,
1995
+ limit: typeof request.body?.limit === "number" ? request.body.limit : undefined,
1996
+ commit: request.body?.commit !== false,
1997
+ cleanup: request.body?.keepWorktree !== true,
1998
+ reviewer: queryString(request.body?.reviewer) ?? "local-bridge-completion",
1999
+ });
2000
+ localSnapshotCache.delete(repoRoot);
2001
+ response.json(result);
2002
+ } catch (error) {
2003
+ response.status(400).json({ error: errorMessage(error) });
2004
+ }
2005
+ });
2006
+ app.post("/api/completion/decision", (request, response) => {
2007
+ try {
2008
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2009
+ const result = applyCompletionDecision(repoRoot, {
2010
+ action: String(request.body?.action ?? "").replace(/-/g, "_") as never,
2011
+ runId: queryString(request.body?.runId) ?? queryString(request.body?.run_id),
2012
+ issueId: queryString(request.body?.issueId) ?? queryString(request.body?.issue_id),
2013
+ taskId: queryString(request.body?.taskId) ?? queryString(request.body?.task_id),
2014
+ reviewer: queryString(request.body?.reviewer) ?? "local-bridge-completion",
2015
+ note: queryString(request.body?.note),
2016
+ commit: request.body?.commit !== false,
2017
+ cleanup: request.body?.keepWorktree !== true,
2018
+ });
2019
+ localSnapshotCache.delete(repoRoot);
2020
+ response.json(result);
2021
+ } catch (error) {
2022
+ response.status(400).json({ error: errorMessage(error) });
2023
+ }
2024
+ });
2025
+ app.get("/api/completion/stuck", (request, response) => {
2026
+ try {
2027
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2028
+ const limit = Number(request.query.limit);
2029
+ response.json(inspectStuckControllerStates(repoRoot, { limit: Number.isFinite(limit) ? limit : 100 }));
2030
+ } catch (error) {
2031
+ response.status(400).json({ error: errorMessage(error) });
2032
+ }
2033
+ });
2034
+ app.post("/api/completion/stuck/migrate", (request, response) => {
2035
+ try {
2036
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2037
+ const result = applyStuckStateMigration(repoRoot, {
2038
+ dryRun: request.body?.apply !== true,
2039
+ limit: typeof request.body?.limit === "number" ? request.body.limit : undefined,
2040
+ reviewer: queryString(request.body?.reviewer) ?? "local-bridge-stuck-migration",
2041
+ markRetryRequired: request.body?.markRetryRequired === true,
2042
+ markNoRunEvidence: request.body?.markNoRunEvidence === true,
2043
+ });
2044
+ localSnapshotCache.delete(repoRoot);
2045
+ response.json(result);
2046
+ } catch (error) {
2047
+ response.status(400).json({ error: errorMessage(error) });
2048
+ }
2049
+ });
2050
+ app.post("/api/controller/codex-continuation", (request, response) => {
2051
+ try {
2052
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2053
+ response.json(prepareCodexContinuation(repoRoot, {
2054
+ objective: queryString(request.body?.objective),
2055
+ maxItems: typeof request.body?.maxItems === "number" ? request.body.maxItems : undefined,
2056
+ mode: request.body?.launch === true ? "launch" : "prepare",
2057
+ }));
2058
+ } catch (error) {
2059
+ response.status(400).json({ error: errorMessage(error) });
2060
+ }
2061
+ });
2062
+ app.get("/api/stream", (request, response) => {
2063
+ try {
2064
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2065
+ response.status(200);
2066
+ response.setHeader("Content-Type", "text/event-stream");
2067
+ response.setHeader("Cache-Control", "no-cache, no-transform");
2068
+ response.setHeader("Connection", "keep-alive");
2069
+ response.flushHeaders();
2070
+ streamClients.set(response, { repoRoot, signature: controllerStateSignature(repoRoot) });
2071
+ sendStreamEvent(response, "connected");
2072
+ request.on("close", () => streamClients.delete(response));
2073
+ } catch (error) {
2074
+ response.status(400).json({ error: errorMessage(error) });
2075
+ }
2076
+ });
2077
+ app.get("/api/progress", (request, response) => {
2078
+ try {
2079
+ response.json(getProjectProgress(requestRepositoryRoot(request, options, controllerHome)));
2080
+ } catch (error) {
2081
+ response.status(400).json({ error: errorMessage(error) });
2082
+ }
2083
+ });
2084
+ app.get("/api/governance", (request, response) => {
2085
+ try {
2086
+ response.json(inspectProjectGovernance(requestRepositoryRoot(request, options, controllerHome)));
2087
+ } catch (error) {
2088
+ response.status(400).json({ error: errorMessage(error) });
2089
+ }
2090
+ });
2091
+ app.post("/api/governance/reconcile", (request, response) => {
2092
+ try {
2093
+ response.json(reconcileProjectGovernance(requestRepositoryRoot(request, options, controllerHome)));
2094
+ } catch (error) {
2095
+ response.status(400).json({ error: errorMessage(error) });
2096
+ }
2097
+ });
2098
+
2099
+ app.get("/api/assistant/openapi.json", (request, response) => {
2100
+ response.json(assistantOpenApiSchema(`${request.protocol}://${request.get("host") ?? "127.0.0.1:8766"}`));
2101
+ });
2102
+
2103
+ app.post("/api/assistant/intent", async (request, response) => {
2104
+ try {
2105
+ const repository = requestRepositorySelection(request, options, controllerHome);
2106
+ response.json(await submitAssistantIntent(controllerHome, repository, {
2107
+ ...(request.body && typeof request.body === "object" && !Array.isArray(request.body) ? request.body as Record<string, unknown> : {}),
2108
+ source: request.body?.source === "mcp" || request.body?.source === "local-ui" || request.body?.source === "mobile" || request.body?.source === "system"
2109
+ ? request.body.source
2110
+ : "chatgpt",
2111
+ }));
2112
+ } catch (error) {
2113
+ response.status(400).json({ error: errorMessage(error) });
2114
+ }
2115
+ });
2116
+
2117
+ app.get("/api/assistant/readiness", (request, response) => {
2118
+ try {
2119
+ const repository = requestRepositorySelection(request, options, controllerHome);
2120
+ response.json(buildAssistantReadinessReport(controllerHome, repository));
2121
+ } catch (error) {
2122
+ response.status(400).json({ error: errorMessage(error) });
2123
+ }
2124
+ });
2125
+
2126
+ app.get("/api/recovery/probe", (request, response) => {
2127
+ try {
2128
+ response.json(cachedLocalControllerSnapshot(requestRepositoryRoot(request, options, controllerHome)).recovery);
2129
+ } catch (error) {
2130
+ response.status(400).json({ error: errorMessage(error) });
2131
+ }
2132
+ });
2133
+
2134
+ app.get("/api/recovery/plan", (request, response) => {
2135
+ try {
2136
+ const recovery = cachedLocalControllerSnapshot(requestRepositoryRoot(request, options, controllerHome)).recovery;
2137
+ response.json({
2138
+ generatedAt: recovery.generatedAt,
2139
+ overallState: recovery.overallState,
2140
+ fallbackRequired: recovery.fallbackRequired,
2141
+ recommendedActions: recovery.recommendedActions,
2142
+ blockingCapabilities: recovery.capabilities.filter((capability) => ["blocked", "unavailable", "degraded"].includes(capability.state)),
2143
+ notes: recovery.notes,
2144
+ });
2145
+ } catch (error) {
2146
+ response.status(400).json({ error: errorMessage(error) });
2147
+ }
2148
+ });
2149
+
2150
+ app.post("/api/recovery/apply", (request, response) => {
2151
+ try {
2152
+ const repository = requestRepositorySelection(request, options, controllerHome);
2153
+ const repoRoot = repository.canonicalRoot;
2154
+ const actionId = queryString(request.body?.actionId) ?? queryString(request.body?.action_id) ?? "";
2155
+ const action = recoveryActionById(actionId);
2156
+ if (!action) throw new Error(`RECOVERY_ACTION_UNKNOWN: ${actionId}`);
2157
+ assertRecoveryAuthorized(action, action.confirmation === "none" ? action.id : request.body?.confirmAuthorization === true ? queryString(request.body?.authorization) : undefined);
2158
+ const reason = queryString(request.body?.reason) ?? "local controller recovery action";
2159
+ let result: Record<string, unknown>;
2160
+ let affectedPaths: string[] = [];
2161
+ if (action.id === "recovery.probe_again") {
2162
+ result = cachedLocalControllerSnapshot(repoRoot).recovery as unknown as Record<string, unknown>;
2163
+ } else if (action.id === "recovery.rebuild_projection" || action.id === "recovery.refresh_repository") {
2164
+ result = { projection: rebuildRepositoryProjection(controllerHome, repository.repoId) };
2165
+ affectedPaths = [".ai/harness/controller/projections"];
2166
+ } else if (action.id === "recovery.cleanup_preview") {
2167
+ result = previewRuntimeCleanup(repoRoot, { includeTempDirs: true, includeTerminalLocalJobs: true, includeLegacyRuns: true, includeHistoricalAttention: true }) as unknown as Record<string, unknown>;
2168
+ } else if (action.id === "recovery.reconcile_jobs") {
2169
+ result = applyRuntimeCleanup(repoRoot, { includeTempDirs: true, includeTerminalLocalJobs: true, includeLegacyRuns: true, includeHistoricalAttention: true, confirmCleanup: true }) as unknown as Record<string, unknown>;
2170
+ affectedPaths = [".ai/harness/local-jobs", ".ai/harness/jobs"];
2171
+ } else {
2172
+ result = { skipped: true, reason: `${action.id} is planned but not executable from the Local Bridge HTTP process.` };
2173
+ }
2174
+ const audit = writeRecoveryAuditRecord(controllerHome, repository.repoId, buildRecoveryAuditRecord({
2175
+ actor: "local-bridge-gui",
2176
+ action,
2177
+ result: result.skipped === true ? "skipped" : "succeeded",
2178
+ reason,
2179
+ affectedPaths,
2180
+ }));
2181
+ localSnapshotCache.delete(repoRoot);
2182
+ response.json({ action, audit, result });
2183
+ } catch (error) {
2184
+ response.status(400).json({ error: errorMessage(error) });
2185
+ }
2186
+ });
2187
+
2188
+ app.post("/api/assistant/self-test/gmail-read", async (request, response) => {
2189
+ try {
2190
+ const repository = requestRepositorySelection(request, options, controllerHome);
2191
+ const query = queryString(request.body?.query) ?? "newer_than:1d";
2192
+ const maxResults = typeof request.body?.maxResults === "number" ? Math.max(1, Math.min(Math.trunc(request.body.maxResults), 10)) : 3;
2193
+ response.status(202).json(await submitAssistantIntent(controllerHome, repository, {
2194
+ utterance: `测试读取 Gmail:${query}`,
2195
+ source: "local-ui",
2196
+ mode: "execute",
2197
+ requestId: queryString(request.body?.requestId),
2198
+ context: { query, max_results: maxResults },
2199
+ }));
2200
+ } catch (error) {
2201
+ response.status(400).json({ error: errorMessage(error) });
2202
+ }
2203
+ });
2204
+
2205
+ app.get("/api/assistant/inbox", (request, response) => {
2206
+ try {
2207
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2208
+ const limit = Number(request.query.limit);
2209
+ response.json(listAssistantInbox(repoRoot, Number.isFinite(limit) ? limit : 50));
2210
+ } catch (error) {
2211
+ response.status(400).json({ error: errorMessage(error) });
2212
+ }
2213
+ });
2214
+
2215
+ app.patch("/api/assistant/inbox/:itemId", (request, response) => {
2216
+ try {
2217
+ const status = request.body?.status;
2218
+ if (!["unread", "read", "archived"].includes(status)) throw new Error("ASSISTANT_INBOX_STATUS_INVALID: status must be unread, read, or archived");
2219
+ response.json({ item: updateAssistantInboxStatus(requestRepositoryRoot(request, options, controllerHome), request.params.itemId, status) });
2220
+ } catch (error) {
2221
+ response.status(400).json({ error: errorMessage(error) });
2222
+ }
2223
+ });
2224
+
2225
+ app.get("/api/assistant/model", (_request, response) => {
2226
+ response.json(assistantModelReadiness());
2227
+ });
2228
+
2229
+ app.get("/api/assistant/standing-grants", (request, response) => {
2230
+ try {
2231
+ const repository = requestRepositorySelection(request, options, controllerHome);
2232
+ response.json(listAssistantStandingGrants(controllerHome, repository, {
2233
+ status: typeof request.query.status === "string" ? request.query.status as any : undefined,
2234
+ limit: Number(request.query.limit) || undefined,
2235
+ }));
2236
+ } catch (error) { response.status(400).json({ error: errorMessage(error) }); }
2237
+ });
2238
+
2239
+ app.post("/api/assistant/standing-grants", (request, response) => {
2240
+ try {
2241
+ const repository = requestRepositorySelection(request, options, controllerHome);
2242
+ response.status(201).json({ grant: createAssistantStandingGrant(controllerHome, repository, {
2243
+ name: queryString(request.body?.name),
2244
+ pluginId: queryString(request.body?.pluginId) ?? "",
2245
+ actionId: queryString(request.body?.actionId) ?? "",
2246
+ routineIds: Array.isArray(request.body?.routineIds) ? request.body.routineIds : undefined,
2247
+ senderAllowlist: Array.isArray(request.body?.senderAllowlist) ? request.body.senderAllowlist : undefined,
2248
+ subjectContains: Array.isArray(request.body?.subjectContains) ? request.body.subjectContains : undefined,
2249
+ minConfidence: typeof request.body?.minConfidence === "number" ? request.body.minConfidence : undefined,
2250
+ maxPerRun: typeof request.body?.maxPerRun === "number" ? request.body.maxPerRun : undefined,
2251
+ expiresInDays: typeof request.body?.expiresInDays === "number" ? request.body.expiresInDays : undefined,
2252
+ confirmAuthorization: request.body?.confirmAuthorization === true,
2253
+ origin: { surface: "local-ui", actor: "assistant-standing-grant-api" },
2254
+ }) });
2255
+ } catch (error) { response.status(400).json({ error: errorMessage(error) }); }
2256
+ });
2257
+
2258
+ app.post("/api/assistant/standing-grants/:grantId/revoke", (request, response) => {
2259
+ try {
2260
+ const repository = requestRepositorySelection(request, options, controllerHome);
2261
+ response.json({ grant: revokeAssistantStandingGrant(controllerHome, repository, {
2262
+ grantId: request.params.grantId,
2263
+ reason: queryString(request.body?.reason),
2264
+ confirmAuthorization: request.body?.confirmAuthorization === true,
2265
+ origin: { surface: "local-ui", actor: "assistant-standing-grant-api" },
2266
+ }) });
2267
+ } catch (error) { response.status(400).json({ error: errorMessage(error) }); }
2268
+ });
2269
+
2270
+ app.get("/api/assistant/proposals", (request, response) => {
2271
+ try {
2272
+ const repository = requestRepositorySelection(request, options, controllerHome);
2273
+ response.json(listAssistantActionProposals(controllerHome, repository, {
2274
+ status: typeof request.query.status === "string" ? request.query.status as any : undefined,
2275
+ limit: Number(request.query.limit) || undefined,
2276
+ }));
2277
+ } catch (error) { response.status(400).json({ error: errorMessage(error) }); }
2278
+ });
2279
+
2280
+ app.post("/api/assistant/proposals/:proposalId/approve", async (request, response) => {
2281
+ try {
2282
+ const repository = requestRepositorySelection(request, options, controllerHome);
2283
+ response.status(202).json({ proposal: await approveAssistantActionProposal(controllerHome, repository, {
2284
+ proposalId: request.params.proposalId,
2285
+ requestId: queryString(request.body?.requestId) ?? `proposal-approval-${request.params.proposalId}`,
2286
+ confirmationText: queryString(request.body?.confirmationText),
2287
+ origin: { surface: 'local-ui', actor: 'assistant-proposal-api' },
2288
+ }) });
2289
+ } catch (error) { response.status(400).json({ error: errorMessage(error) }); }
2290
+ });
2291
+
2292
+ app.post("/api/assistant/proposals/:proposalId/reject", (request, response) => {
2293
+ try {
2294
+ const repository = requestRepositorySelection(request, options, controllerHome);
2295
+ response.json({ proposal: rejectAssistantActionProposal(controllerHome, repository, request.params.proposalId, queryString(request.body?.reason)) });
2296
+ } catch (error) { response.status(400).json({ error: errorMessage(error) }); }
2297
+ });
2298
+
2299
+ app.get("/api/assistant/routines", (request, response) => {
2300
+ try {
2301
+ response.json(listAssistantRoutines(requestRepositoryRoot(request, options, controllerHome)));
2302
+ } catch (error) {
2303
+ response.status(400).json({ error: errorMessage(error) });
2304
+ }
2305
+ });
2306
+
2307
+ app.post("/api/assistant/routines", async (request, response) => {
2308
+ try {
2309
+ const repository = requestRepositorySelection(request, options, controllerHome);
2310
+ const result = await submitAssistantIntent(controllerHome, repository, {
2311
+ utterance: queryString(request.body?.naturalLanguageGoal) ?? queryString(request.body?.utterance) ?? queryString(request.body?.name) ?? "create routine",
2312
+ source: "chatgpt",
2313
+ mode: "plan_then_execute",
2314
+ confirmRoutine: request.body?.confirmRoutine === true,
2315
+ timezone: queryString(request.body?.timezone),
2316
+ routine: {
2317
+ name: queryString(request.body?.name),
2318
+ naturalLanguageGoal: queryString(request.body?.naturalLanguageGoal),
2319
+ scheduleText: queryString(request.body?.scheduleText),
2320
+ timezone: queryString(request.body?.timezone),
2321
+ dataSources: Array.isArray(request.body?.dataSources) ? request.body.dataSources : undefined,
2322
+ output: request.body?.output,
2323
+ allowedActions: Array.isArray(request.body?.allowedActions) ? request.body.allowedActions : undefined,
2324
+ forbiddenActions: Array.isArray(request.body?.forbiddenActions) ? request.body.forbiddenActions : undefined,
2325
+ },
2326
+ });
2327
+ response.status(result.routine ? 201 : 200).json(result);
2328
+ } catch (error) {
2329
+ response.status(400).json({ error: errorMessage(error) });
2330
+ }
2331
+ });
2332
+
2333
+ app.post("/api/assistant/routines/:routineId/run", (request, response) => {
2334
+ try {
2335
+ const repository = requestRepositorySelection(request, options, controllerHome);
2336
+ response.status(202).json(runAssistantRoutineNow(controllerHome, repository, request.params.routineId));
2337
+ } catch (error) {
2338
+ response.status(400).json({ error: errorMessage(error) });
2339
+ }
2340
+ });
2341
+
2342
+ app.post("/api/assistant/routines/:routineId/pause", (request, response) => {
2343
+ try {
2344
+ const repository = requestRepositorySelection(request, options, controllerHome);
2345
+ response.json(updateAssistantRoutineLifecycle(controllerHome, repository, request.params.routineId, "paused"));
2346
+ } catch (error) {
2347
+ response.status(400).json({ error: errorMessage(error) });
2348
+ }
2349
+ });
2350
+
2351
+ app.post("/api/assistant/routines/:routineId/resume", (request, response) => {
2352
+ try {
2353
+ const repository = requestRepositorySelection(request, options, controllerHome);
2354
+ response.json(updateAssistantRoutineLifecycle(controllerHome, repository, request.params.routineId, "enabled"));
2355
+ } catch (error) {
2356
+ response.status(400).json({ error: errorMessage(error) });
2357
+ }
2358
+ });
2359
+
2360
+ app.post("/api/assistant/routines/:routineId/delete", (request, response) => {
2361
+ try {
2362
+ const repository = requestRepositorySelection(request, options, controllerHome);
2363
+ response.json(updateAssistantRoutineLifecycle(controllerHome, repository, request.params.routineId, "deleted"));
2364
+ } catch (error) {
2365
+ response.status(400).json({ error: errorMessage(error) });
2366
+ }
2367
+ });
2368
+
2369
+ app.post("/api/assistant/maintenance/cleanup-preview", (request, response) => {
2370
+ try {
2371
+ response.json(previewRuntimeCleanup(requestRepositoryRoot(request, options, controllerHome), {
2372
+ minAgeMinutes: typeof request.body?.minAgeMinutes === "number" ? request.body.minAgeMinutes : undefined,
2373
+ includeTempDirs: request.body?.includeTempDirs !== false,
2374
+ includeTerminalLocalJobs: request.body?.includeTerminalLocalJobs === true,
2375
+ includeLegacyRuns: request.body?.includeLegacyRuns === true,
2376
+ includeHistoricalAttention: request.body?.includeHistoricalAttention === true,
2377
+ maxCandidates: typeof request.body?.maxCandidates === "number" ? request.body.maxCandidates : undefined,
2378
+ }));
2379
+ } catch (error) {
2380
+ response.status(400).json({ error: errorMessage(error) });
2381
+ }
2382
+ });
2383
+
2384
+ app.post("/api/assistant/maintenance/cleanup-apply", (request, response) => {
2385
+ try {
2386
+ response.json(applyRuntimeCleanup(requestRepositoryRoot(request, options, controllerHome), {
2387
+ minAgeMinutes: typeof request.body?.minAgeMinutes === "number" ? request.body.minAgeMinutes : undefined,
2388
+ includeTempDirs: request.body?.includeTempDirs !== false,
2389
+ includeTerminalLocalJobs: request.body?.includeTerminalLocalJobs === true,
2390
+ includeLegacyRuns: request.body?.includeLegacyRuns === true,
2391
+ includeHistoricalAttention: request.body?.includeHistoricalAttention === true,
2392
+ maxCandidates: typeof request.body?.maxCandidates === "number" ? request.body.maxCandidates : undefined,
2393
+ confirmCleanup: request.body?.confirmCleanup === true,
2394
+ }));
2395
+ } catch (error) {
2396
+ response.status(400).json({ error: errorMessage(error) });
2397
+ }
2398
+ });
2399
+
2400
+ app.get("/api/assistant/memory", (request, response) => {
2401
+ try {
2402
+ response.json(listAssistantMemory(requestRepositoryRoot(request, options, controllerHome)));
2403
+ } catch (error) {
2404
+ response.status(400).json({ error: errorMessage(error) });
2405
+ }
2406
+ });
2407
+
2408
+ app.post("/api/assistant/memory", (request, response) => {
2409
+ try {
2410
+ response.json({ entry: upsertAssistantMemory(requestRepositoryRoot(request, options, controllerHome), {
2411
+ key: queryString(request.body?.key) ?? "",
2412
+ value: queryString(request.body?.value) ?? "",
2413
+ source: queryString(request.body?.source) ?? "chatgpt",
2414
+ }) });
2415
+ } catch (error) {
2416
+ response.status(400).json({ error: errorMessage(error) });
2417
+ }
2418
+ });
2419
+
2420
+ app.get("/api/mobile/devices", (request, response) => {
2421
+ try {
2422
+ response.json(listMobileIntentDevices(requestRepositoryRoot(request, options, controllerHome)));
2423
+ } catch (error) {
2424
+ response.status(400).json({ error: errorMessage(error) });
2425
+ }
2426
+ });
2427
+ app.post("/api/mobile/devices", (request, response) => {
2428
+ try {
2429
+ response.status(201).json(createMobileIntentDevice(requestRepositoryRoot(request, options, controllerHome), {
2430
+ name: queryString(request.body?.name),
2431
+ deviceId: queryString(request.body?.deviceId),
2432
+ scopes: request.body?.scopes,
2433
+ rateLimitPerMinute: request.body?.rateLimitPerMinute,
2434
+ }));
2435
+ } catch (error) {
2436
+ response.status(400).json({ error: errorMessage(error) });
2437
+ }
2438
+ });
2439
+ app.post("/api/mobile/devices/:deviceId/revoke", (request, response) => {
2440
+ try {
2441
+ response.json(revokeMobileIntentDevice(requestRepositoryRoot(request, options, controllerHome), request.params.deviceId));
2442
+ } catch (error) {
2443
+ response.status(404).json({ error: errorMessage(error) });
2444
+ }
2445
+ });
2446
+ app.get("/api/project-state", (request, response) => {
2447
+ try {
2448
+ response.json(loadControllerProjectState(requestRepositoryRoot(request, options, controllerHome)));
2449
+ } catch (error) {
2450
+ response.status(400).json({ error: errorMessage(error) });
2451
+ }
2452
+ });
2453
+ app.patch("/api/project-state", (request, response) => {
2454
+ try {
2455
+ const body = request.body ?? {};
2456
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2457
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2458
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2459
+ return;
2460
+ }
2461
+ if (body.currentIssueId === null || body.currentIssueId === "") {
2462
+ response.json(clearCurrentIssue(repoRoot, "local-ui"));
2463
+ return;
2464
+ }
2465
+ if (typeof body.currentIssueId === "string") {
2466
+ const issue = getIssue(repoRoot, body.currentIssueId);
2467
+ if (issue.archivedAt || ["done", "cancelled"].includes(issue.status)) throw new Error("only an active, non-archived Issue can become the execution focus");
2468
+ }
2469
+ response.json(saveControllerProjectState(repoRoot, {
2470
+ currentIssueId: typeof body.currentIssueId === "string" ? body.currentIssueId : undefined,
2471
+ issueCreationMode: ["open", "focus_only", "paused"].includes(body.issueCreationMode) ? body.issueCreationMode : undefined,
2472
+ showArchivedByDefault: typeof body.showArchivedByDefault === "boolean" ? body.showArchivedByDefault : undefined,
2473
+ }, "local-ui"));
2474
+ } catch (error) {
2475
+ response.status(400).json({ error: errorMessage(error) });
2476
+ }
2477
+ });
2478
+ app.post("/api/issues/:issueId/focus", (request, response) => {
2479
+ try {
2480
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2481
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2482
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2483
+ return;
2484
+ }
2485
+ const issue = getIssue(repoRoot, request.params.issueId);
2486
+ if (issue.archivedAt || ["done", "cancelled"].includes(issue.status)) throw new Error("only an active, non-archived Issue can become the execution focus");
2487
+ response.json(saveControllerProjectState(repoRoot, { currentIssueId: issue.id }, "local-ui"));
2488
+ } catch (error) {
2489
+ response.status(400).json({ error: errorMessage(error) });
2490
+ }
2491
+ });
2492
+ app.post("/api/tasks/launch-ready", (_request, response) => {
2493
+ response.status(410).json(localBridgeExecutionRetiredPayload());
2494
+ });
2495
+ app.post("/api/issues/:issueId/launch", (_request, response) => {
2496
+ response.status(410).json(localBridgeExecutionRetiredPayload());
2497
+ });
2498
+ app.post("/api/issues/:issueId/archive", (request, response) => {
2499
+ try {
2500
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2501
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2502
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2503
+ return;
2504
+ }
2505
+ response.json(archiveIssue(repoRoot, request.params.issueId));
2506
+ } catch (error) {
2507
+ response.status(400).json({ error: errorMessage(error) });
2508
+ }
2509
+ });
2510
+ app.post("/api/issues/:issueId/restore", (request, response) => {
2511
+ try {
2512
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2513
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2514
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2515
+ return;
2516
+ }
2517
+ response.json(restoreIssue(repoRoot, request.params.issueId));
2518
+ } catch (error) {
2519
+ response.status(400).json({ error: errorMessage(error) });
2520
+ }
2521
+ });
2522
+ app.post("/api/issues/:issueId/tasks/:taskId/launch", (_request, response) => {
2523
+ response.status(410).json(localBridgeExecutionRetiredPayload());
2524
+ });
2525
+ app.post("/api/issues/:issueId/tasks/:taskId/verify", async (request, response) => {
2526
+ try {
2527
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2528
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2529
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2530
+ return;
2531
+ }
2532
+ const issue = getIssue(repoRoot, request.params.issueId);
2533
+ const task = issue.tasks.find((entry) => entry.id === request.params.taskId);
2534
+ if (!task) throw new Error("task not found");
2535
+ if (task.status === "done" || task.status === "verified") {
2536
+ response.json(issue);
2537
+ return;
2538
+ }
2539
+ const policy = taskExecutionPolicy(task);
2540
+ const confirmAcceptance = request.body?.confirmAcceptance === true;
2541
+ const reviewer = queryString(request.body?.reviewer) ?? "local-controller-human";
2542
+ const latestRunId = task.runIds.at(-1);
2543
+ if (latestRunId) {
2544
+ const run = getAgentJob(repoRoot, latestRunId);
2545
+ if (run.status !== "succeeded") throw new Error(`verification requires a succeeded Run (current: ${run.status})`);
2546
+ if (run.provider === "local" && run.worktree !== repoRoot && !run.integratedSessionId) throw new Error("integrate the isolated local Run before verification");
2547
+ if (run.provider === "github" && run.github?.createPullRequest !== false && !run.github?.pullRequestUrl) throw new Error("GitHub verification requires the linked pull request");
2548
+ continueTaskAfterSuccessfulRun(repoRoot, run);
2549
+ if (!confirmAcceptance) {
2550
+ response.json(getIssue(repoRoot, issue.id));
2551
+ return;
2552
+ }
2553
+ const refreshedIssue = getIssue(repoRoot, issue.id);
2554
+ const refreshedTask = refreshedIssue.tasks.find((entry) => entry.id === task.id);
2555
+ if (!refreshedTask?.verification) throw new Error("successful Run did not produce verification evidence");
2556
+ const at = new Date().toISOString();
2557
+ response.json(recordTaskVerification(repoRoot, issue.id, task.id, {
2558
+ ...refreshedTask.verification,
2559
+ reviewer,
2560
+ acceptanceResults: task.acceptanceCriteria.map((criterion) => ({
2561
+ criterion,
2562
+ ok: true,
2563
+ outcome: "passed" as const,
2564
+ source: "human_review" as const,
2565
+ evidence: `Explicitly confirmed in the local Controller at ${at}.`,
2566
+ })),
2567
+ verifiedAt: at,
2568
+ }));
2569
+ return;
2570
+ }
2571
+
2572
+ // Manual evidence-only verification remains available for Tasks that do not
2573
+ // require a Run or Diff. Missing named checks never block launch or this path.
2574
+ const checkResults = await Promise.all(task.checks.map(async (checkId) => {
2575
+ try {
2576
+ const result = await runControllerCheckAsync(repoRoot, checkId);
2577
+ return {
2578
+ checkId,
2579
+ ok: result.ok,
2580
+ summary: `${result.ok ? "Passed" : "Failed"} with persisted evidence ${result.artifactPath}`,
2581
+ };
2582
+ } catch (error) {
2583
+ return { checkId, ok: false, summary: errorMessage(error) };
2584
+ }
2585
+ }));
2586
+ if (policy.requiresAcceptanceEvidence && !confirmAcceptance) {
2587
+ throw new Error(`${policy.executionClass} requires explicit acceptance evidence when no successful Run is linked`);
2588
+ }
2589
+ const at = new Date().toISOString();
2590
+ response.json(recordTaskVerification(repoRoot, issue.id, task.id, {
2591
+ reviewedDiffHash: typeof request.body?.reviewedDiffHash === "string" ? request.body.reviewedDiffHash : undefined,
2592
+ reviewer,
2593
+ checkResults,
2594
+ commandEvidence: [],
2595
+ acceptanceResults: confirmAcceptance
2596
+ ? task.acceptanceCriteria.map((criterion) => ({
2597
+ criterion,
2598
+ ok: true,
2599
+ outcome: "passed" as const,
2600
+ source: "human_review" as const,
2601
+ evidence: `Explicitly confirmed in the local Controller at ${at}.`,
2602
+ }))
2603
+ : [],
2604
+ verifiedAt: at,
2605
+ }));
2606
+ } catch (error) {
2607
+ response.status(400).json({ error: errorMessage(error) });
2608
+ }
2609
+ });
2610
+ app.post("/api/issues/:issueId/tasks/:taskId/accept", (request, response) => {
2611
+ try {
2612
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2613
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2614
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2615
+ return;
2616
+ }
2617
+ const issue = getIssue(repoRoot, request.params.issueId);
2618
+ const task = issue.tasks.find((entry) => entry.id === request.params.taskId);
2619
+ if (!task) throw new Error("task not found");
2620
+ const latestRunId = task.runIds.at(-1);
2621
+ if (latestRunId) {
2622
+ const run = getAgentJob(repoRoot, latestRunId);
2623
+ if (run.status !== "succeeded") throw new Error(`latest Run must succeed before acceptance (current: ${run.status})`);
2624
+ if (run.provider === "local" && run.worktree !== repoRoot && !run.integratedSessionId) {
2625
+ throw new Error("integrate the isolated local Task Run before accepting it");
2626
+ }
2627
+ }
2628
+ response.json(acceptVerifiedTask(repoRoot, request.params.issueId, request.params.taskId, queryString(request.body?.note) ?? "Accepted from the local Controller."));
2629
+ } catch (error) {
2630
+ response.status(400).json({ error: errorMessage(error) });
2631
+ }
2632
+ });
2633
+ app.post("/api/issues/:issueId/tasks/:taskId/request-changes", (request, response) => {
2634
+ try {
2635
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2636
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2637
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2638
+ return;
2639
+ }
2640
+ const issue = getIssue(repoRoot, request.params.issueId);
2641
+ const task = issue.tasks.find((entry) => entry.id === request.params.taskId);
2642
+ if (!task) throw new Error("task not found");
2643
+ if (!["review", "integrated", "verifying", "verified"].includes(task.status)) throw new Error(`Task is not reviewable from ${task.status}`);
2644
+ response.json(updateTask(repoRoot, issue.id, task.id, { status: "changes_requested", note: queryString(request.body?.note) ?? "Changes requested from the local Controller." }));
2645
+ } catch (error) {
2646
+ response.status(400).json({ error: errorMessage(error) });
2647
+ }
2648
+ });
2649
+ app.post("/api/issues/:issueId/tasks/:taskId/cancel", (request, response) => {
2650
+ try {
2651
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2652
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2653
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2654
+ return;
2655
+ }
2656
+ const issue = getIssue(repoRoot, request.params.issueId);
2657
+ const task = issue.tasks.find((entry) => entry.id === request.params.taskId);
2658
+ if (!task) throw new Error("task not found");
2659
+ const state = resolveEffectiveTaskState({ issue, task, runs: readTaskRunEvidence(repoRoot, task) });
2660
+ if (state.activeRunIds.length > 0) throw new Error(`cancel active Run(s) ${state.activeRunIds.join(", ")} before cancelling the Task`);
2661
+ response.json(updateTask(repoRoot, request.params.issueId, request.params.taskId, { status: "cancelled", note: queryString(request.body?.note) ?? "Task cancelled from the local Controller." }));
2662
+ } catch (error) {
2663
+ response.status(400).json({ error: errorMessage(error) });
2664
+ }
2665
+ });
2666
+ app.post("/api/issues/:issueId/tasks/:taskId/dependencies", (request, response) => {
2667
+ try {
2668
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2669
+ if (legacyIssueAuthorityRetired(repoRoot)) {
2670
+ response.status(410).json(legacyControlPlaneMutationRetiredPayload());
2671
+ return;
2672
+ }
2673
+ if (!Array.isArray(request.body?.dependsOn)) throw new Error("dependsOn must be an array");
2674
+ response.json(setTaskDependencies(repoRoot, request.params.issueId, request.params.taskId, request.body.dependsOn.map(String)));
2675
+ } catch (error) {
2676
+ response.status(400).json({ error: errorMessage(error) });
2677
+ }
2678
+ });
2679
+ app.get("/api/recipes/:recipeId/prepare", (request, response) => {
2680
+ try {
2681
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2682
+ const ledger = buildControllerTaskLedgerProjection(repoRoot);
2683
+ const plan = buildControllerOperationalPlan(repoRoot, ledger);
2684
+ const recipe = plan.recipeSystem.recipes.find((entry) => entry.id === request.params.recipeId);
2685
+ if (!recipe) throw new Error(`recipe not found: ${request.params.recipeId}`);
2686
+ response.json({
2687
+ recipe,
2688
+ status: plan.status,
2689
+ recommendedWorker: plan.workerAbstraction.recommendedWorker,
2690
+ diffProjection: plan.diffProjection,
2691
+ validationStrategy: plan.validationStrategy,
2692
+ taskRecovery: plan.taskRecovery,
2693
+ next: recipe.steps.map((step, index) => ({ step, index: index + 1, done: false })),
2694
+ });
2695
+ } catch (error) {
2696
+ response.status(404).json({ error: errorMessage(error) });
2697
+ }
2698
+ });
2699
+
2700
+ app.post("/api/checks/run", (request, response) => {
2701
+ try {
2702
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2703
+ const requested = Array.isArray(request.body?.checkIds) ? request.body.checkIds.map(String) : [];
2704
+ const ids: string[] = requested.length > 0 ? requested.slice(0, 8) : ["package:check:type"];
2705
+ const results = ids.map((checkId: string) => {
2706
+ const result = runControllerCheck(repoRoot, checkId, typeof request.body?.timeoutMs === "number" ? request.body.timeoutMs : undefined);
2707
+ return {
2708
+ checkId,
2709
+ ok: result.ok,
2710
+ status: result.status,
2711
+ timedOut: result.timedOut,
2712
+ artifactPath: result.artifactPath,
2713
+ executedAt: result.executedAt,
2714
+ };
2715
+ });
2716
+ response.json({ ok: results.every((entry: { ok: boolean }) => entry.ok), results });
2717
+ } catch (error) {
2718
+ response.status(400).json({ error: errorMessage(error) });
2719
+ }
2720
+ });
2721
+ app.get("/api/timeline", (request, response) => {
2722
+ try {
2723
+ response.json({
2724
+ events: getControllerTimeline(requestRepositoryRoot(request, options, controllerHome), {
2725
+ category: parseWorklogCategory(queryString(request.query.category)),
2726
+ issueId: queryString(request.query.issueId),
2727
+ taskId: queryString(request.query.taskId),
2728
+ runId: queryString(request.query.runId),
2729
+ editSessionId: queryString(request.query.editSessionId),
2730
+ since: queryString(request.query.since),
2731
+ until: queryString(request.query.until),
2732
+ limit: request.query.limit ? Number(request.query.limit) : 300,
2733
+ }),
2734
+ });
2735
+ } catch (error) {
2736
+ response.status(400).json({ error: errorMessage(error) });
2737
+ }
2738
+ });
2739
+ app.get("/api/issues/:issueId/tasks/:taskId", (request, response) => {
2740
+ try {
2741
+ response.json(
2742
+ getTaskProgressDetail(
2743
+ requestRepositoryRoot(request, options, controllerHome),
2744
+ request.params.issueId,
2745
+ request.params.taskId,
2746
+ ),
2747
+ );
2748
+ } catch (error) {
2749
+ response.status(404).json({ error: errorMessage(error) });
2750
+ }
2751
+ });
2752
+ app.post("/api/worklog/export", (request, response) => {
2753
+ try {
2754
+ response.json(
2755
+ exportControllerWorklog(requestRepositoryRoot(request, options, controllerHome), {
2756
+ format: request.body?.format === "json" ? "json" : "markdown",
2757
+ outputPath: queryString(request.body?.outputPath),
2758
+ filter: {
2759
+ issueId: queryString(request.body?.issueId),
2760
+ taskId: queryString(request.body?.taskId),
2761
+ runId: queryString(request.body?.runId),
2762
+ editSessionId: queryString(request.body?.editSessionId),
2763
+ },
2764
+ }),
2765
+ );
2766
+ } catch (error) {
2767
+ response.status(400).json({ error: errorMessage(error) });
2768
+ }
2769
+ });
2770
+ app.get("/api/edit-sessions", (request, response) => {
2771
+ try {
2772
+ const limit = request.query.limit ? Number(request.query.limit) : 200;
2773
+ response.json({ sessions: listEditSessions(requestRepositoryRoot(request, options, controllerHome), limit) });
2774
+ } catch (error) {
2775
+ response.status(400).json({ error: errorMessage(error) });
2776
+ }
2777
+ });
2778
+ app.get("/api/edit-sessions/:sessionId", (request, response) => {
2779
+ try {
2780
+ response.json(getEditSession(requestRepositoryRoot(request, options, controllerHome), request.params.sessionId));
2781
+ } catch (error) {
2782
+ response.status(404).json({ error: errorMessage(error) });
2783
+ }
2784
+ });
2785
+ app.get("/api/edit-sessions/:sessionId/diff", (request, response) => {
2786
+ try {
2787
+ response.json(getEditSessionDiff(requestRepositoryRoot(request, options, controllerHome), request.params.sessionId));
2788
+ } catch (error) {
2789
+ response.status(404).json({ error: errorMessage(error) });
2790
+ }
2791
+ });
2792
+ app.post("/api/edit-sessions/:sessionId/savepoints", (request, response) => {
2793
+ try {
2794
+ response.json(createEditSavepoint(requestRepositoryRoot(request, options, controllerHome), request.params.sessionId, String(request.body?.name ?? "")));
2795
+ } catch (error) {
2796
+ response.status(400).json({ error: errorMessage(error) });
2797
+ }
2798
+ });
2799
+ app.post("/api/edit-sessions/:sessionId/verify", (request, response) => {
2800
+ try {
2801
+ // Deterministic Direct Edit verification no longer creates a Local Bridge Job.
2802
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2803
+ const session = getEditSession(repoRoot, request.params.sessionId);
2804
+ const verified = verifyEditSession(repoRoot, session.sessionId, {
2805
+ checkIds: Array.isArray(request.body?.checkIds) ? request.body.checkIds.map(String) : undefined,
2806
+ reviewer: queryString(request.body?.reviewer) ?? "local-controller-human",
2807
+ note: queryString(request.body?.note),
2808
+ });
2809
+ response.status(200).json({
2810
+ accepted: true,
2811
+ status: "succeeded",
2812
+ sessionId: verified.sessionId,
2813
+ revision: verified.currentRevision,
2814
+ checkResults: verified.checkResults ?? [],
2815
+ verifiedAt: verified.verifiedAt ?? null,
2816
+ });
2817
+ } catch (error) {
2818
+ response.status(400).json({ error: errorMessage(error) });
2819
+ }
2820
+ });
2821
+ app.post("/api/edit-sessions/:sessionId/finalize", (request, response) => {
2822
+ try {
2823
+ response.json(finalizeEditSession(requestRepositoryRoot(request, options, controllerHome), request.params.sessionId, {
2824
+ reviewer: queryString(request.body?.reviewer) ?? "local-controller-human",
2825
+ note: queryString(request.body?.note),
2826
+ }));
2827
+ } catch (error) {
2828
+ response.status(400).json({ error: errorMessage(error) });
2829
+ }
2830
+ });
2831
+ app.post("/api/edit-sessions/:sessionId/finish", (request, response) => {
2832
+ try {
2833
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
2834
+ const result = finishEditSession(repoRoot, {
2835
+ sessionId: request.params.sessionId,
2836
+ decision: String(request.body?.decision ?? "auto").replace(/-/g, "_") as never,
2837
+ reviewer: queryString(request.body?.reviewer) ?? "local-bridge-direct-edit",
2838
+ note: queryString(request.body?.note),
2839
+ checkIds: Array.isArray(request.body?.checkIds) ? request.body.checkIds.map(String) : undefined,
2840
+ commit: request.body?.commit !== false,
2841
+ });
2842
+ localSnapshotCache.delete(repoRoot);
2843
+ response.json(result);
2844
+ } catch (error) {
2845
+ response.status(400).json({ error: errorMessage(error) });
2846
+ }
2847
+ });
2848
+ app.post("/api/edit-sessions/:sessionId/rollback", (request, response) => {
2849
+ try {
2850
+ response.json(rollbackEditSession(requestRepositoryRoot(request, options, controllerHome), request.params.sessionId, {
2851
+ toRevision: typeof request.body?.toRevision === "number" ? Math.trunc(request.body.toRevision) : undefined,
2852
+ savepoint: queryString(request.body?.savepoint),
2853
+ }));
2854
+ } catch (error) {
2855
+ response.status(400).json({ error: errorMessage(error) });
2856
+ }
2857
+ });
2858
+ app.get("/api/github/plugin", (request, response) => {
2859
+ try {
2860
+ response.json(getGitHubPluginStatus(requestRepositoryRoot(request, options, controllerHome)));
2861
+ } catch (error) {
2862
+ response.status(400).json({ error: errorMessage(error) });
2863
+ }
2864
+ });
2865
+ app.get("/api/toolchain/plugins/:pluginId/summary", (request, response) => {
2866
+ try {
2867
+ const repository = requestRepositorySelection(request, options, controllerHome);
2868
+ const manifest = getAssistantPluginManifest(controllerHome, repository, request.params.pluginId);
2869
+ response.json({ plugin: summarizePluginForLowInterception(manifest) });
2870
+ } catch (error) {
2871
+ response.status(404).json({ error: errorMessage(error) });
2872
+ }
2873
+ });
2874
+ app.get("/api/toolchain/web-targets", (request, response) => {
2875
+ try {
2876
+ const repository = requestRepositorySelection(request, options, controllerHome);
2877
+ const manifest = getAssistantPluginManifest(controllerHome, repository, "browser");
2878
+ response.json({ targets: listWebTargets(repository.canonicalRoot, manifest), arbitraryUrlAccepted: false });
2879
+ } catch (error) {
2880
+ response.status(400).json({ error: errorMessage(error) });
2881
+ }
2882
+ });
2883
+ app.post("/api/toolchain/web-domain-preview", (request, response) => {
2884
+ try {
2885
+ const repository = requestRepositorySelection(request, options, controllerHome);
2886
+ const manifest = getAssistantPluginManifest(controllerHome, repository, "browser");
2887
+ response.json({ preview: previewBrowserDomainAccess(repository.canonicalRoot, request.body?.domain, request.body?.reason, manifest) });
2888
+ } catch (error) {
2889
+ response.status(400).json({ error: errorMessage(error) });
2890
+ }
2891
+ });
2892
+ app.get("/api/toolchain/model-clients", (_request, response) => {
2893
+ response.json({ clients: buildModelClientSummary(), policyOwner: "forge" });
2894
+ });
2895
+ app.get("/api/toolchain/model-control-plane", (_request, response) => {
2896
+ response.json({ controlPlane: buildModelControlPlaneSummary(), transportEncryption: "not-configured-by-this-tool" });
2897
+ });
2898
+ app.get("/api/toolchain/deepseek/tools", (_request, response) => {
2899
+ response.json({ provider: "deepseek", tools: deepSeekFunctionToolManifest(), policyOwner: "forge" });
2900
+ });
2901
+ app.get("/api/toolchain/deepseek/controller-manifest", (_request, response) => {
2902
+ response.json({ manifest: deepSeekControllerManifest() });
2903
+ });
2904
+ app.post("/api/toolchain/deepseek/prepare", (request, response) => {
2905
+ try {
2906
+ const args = request.body?.functionArguments && typeof request.body.functionArguments === "object" && !Array.isArray(request.body.functionArguments)
2907
+ ? request.body.functionArguments as Record<string, unknown>
2908
+ : {};
2909
+ response.json({ prepared: prepareDeepSeekToolCall(String(request.body?.functionName ?? "").trim(), args) });
2910
+ } catch (error) {
2911
+ response.status(400).json({ error: errorMessage(error) });
2912
+ }
2913
+ });
2914
+ app.post("/api/toolchain/deepseek/handoff", (request, response) => {
2915
+ try {
2916
+ const repository = requestRepositorySelection(request, options, controllerHome);
2917
+ response.json({ handoff: prepareDeepSeekControllerHandoff({
2918
+ reason: request.body?.reason,
2919
+ objective: request.body?.objective,
2920
+ repoId: repository.repoId,
2921
+ currentController: request.body?.currentController,
2922
+ blockedToolName: request.body?.blockedToolName,
2923
+ recentSafeError: request.body?.recentSafeError,
2924
+ }) });
2925
+ } catch (error) {
2926
+ response.status(400).json({ error: errorMessage(error) });
2927
+ }
2928
+ });
2929
+ app.post("/api/toolchain/deepseek/controller-request", (request, response) => {
2930
+ try {
2931
+ const repository = requestRepositorySelection(request, options, controllerHome);
2932
+ response.json({ preview: prepareDeepSeekControllerRequest({
2933
+ reason: request.body?.reason,
2934
+ objective: request.body?.objective,
2935
+ userMessage: request.body?.userMessage,
2936
+ repoId: repository.repoId,
2937
+ currentController: request.body?.currentController,
2938
+ blockedToolName: request.body?.blockedToolName,
2939
+ recentSafeError: request.body?.recentSafeError,
2940
+ model: request.body?.model,
2941
+ }) });
2942
+ } catch (error) {
2943
+ response.status(400).json({ error: errorMessage(error) });
2944
+ }
2945
+ });
2946
+ app.get("/api/plugins", (request, response) => {
2947
+ try {
2948
+ const repository = requestRepositorySelection(request, options, controllerHome);
2949
+ response.json({ plugins: listAssistantPluginManifests(controllerHome, repository) });
2950
+ } catch (error) {
2951
+ response.status(400).json({ error: errorMessage(error) });
2952
+ }
2953
+ });
2954
+ app.get("/api/plugins/:pluginId", (request, response) => {
2955
+ try {
2956
+ const repository = requestRepositorySelection(request, options, controllerHome);
2957
+ response.json({ plugin: getAssistantPluginManifest(controllerHome, repository, request.params.pluginId) });
2958
+ } catch (error) {
2959
+ response.status(404).json({ error: errorMessage(error) });
2960
+ }
2961
+ });
2962
+ app.post("/api/plugins/:pluginId/actions/:actionId", async (request, response) => {
2963
+ try {
2964
+ const repository = requestRepositorySelection(request, options, controllerHome);
2965
+ const submitted = await submitAssistantPluginAction(controllerHome, repository, {
2966
+ pluginId: request.params.pluginId,
2967
+ actionId: request.params.actionId,
2968
+ requestId: queryString(request.body?.requestId) ?? '',
2969
+ args: request.body?.arguments && typeof request.body.arguments === "object" && !Array.isArray(request.body.arguments)
2970
+ ? request.body.arguments as Record<string, unknown>
2971
+ : {},
2972
+ timeoutMs: typeof request.body?.timeoutMs === "number" ? request.body.timeoutMs : undefined,
2973
+ confirmAuthorization: request.body?.confirmAuthorization === true,
2974
+ confirmationText: queryString(request.body?.confirmationText),
2975
+ origin: { surface: "local-ui", actor: "local-controller", correlationId: queryString(request.body?.requestId) },
2976
+ });
2977
+ response.status(202).json({
2978
+ accepted: true,
2979
+ deduplicated: submitted.deduplicated,
2980
+ plugin: submitted.manifest,
2981
+ action: {
2982
+ actionId: submitted.action.actionId,
2983
+ risk: submitted.action.risk,
2984
+ confirmation: submitted.action.confirmation,
2985
+ requiredConfirmationText: submitted.action.requiredConfirmationText,
2986
+ },
2987
+ job: submitted.job,
2988
+ });
2989
+ } catch (error) {
2990
+ response.status(400).json({ error: errorMessage(error) });
2991
+ }
2992
+ });
2993
+ app.patch("/api/github/plugin", (request, response) => {
2994
+ try {
2995
+ const body = request.body ?? {};
2996
+ response.json(
2997
+ saveGitHubPluginConfig(requestRepositoryRoot(request, options, controllerHome), {
2998
+ enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
2999
+ repository: typeof body.repository === "string" ? body.repository : undefined,
3000
+ syncMode: body.syncMode === "checkpoint" ? "checkpoint" : body.syncMode === "manual" ? "manual" : undefined,
3001
+ includeTasks: typeof body.includeTasks === "boolean" ? body.includeTasks : undefined,
3002
+ projectOwner: typeof body.projectOwner === "string" ? body.projectOwner : undefined,
3003
+ projectNumber: body.projectNumber === null ? null : typeof body.projectNumber === "number" ? body.projectNumber : undefined,
3004
+ }),
3005
+ );
3006
+ } catch (error) {
3007
+ response.status(400).json({ error: errorMessage(error) });
3008
+ }
3009
+ });
3010
+ app.post("/api/issues/:issueId/github/publish", (request, response) => {
3011
+ try {
3012
+ response.json(publishIssueWithGitHubPlugin(requestRepositoryRoot(request, options, controllerHome), request.params.issueId));
3013
+ } catch (error) {
3014
+ response.status(400).json({ error: errorMessage(error) });
3015
+ }
3016
+ });
3017
+ app.post("/api/issues/:issueId/github/refresh", (request, response) => {
3018
+ try {
3019
+ response.json(refreshIssueWithGitHubPlugin(requestRepositoryRoot(request, options, controllerHome), request.params.issueId));
3020
+ } catch (error) {
3021
+ response.status(400).json({ error: errorMessage(error) });
3022
+ }
3023
+ });
3024
+ app.post("/api/issues/:issueId/github/close", (request, response) => {
3025
+ try {
3026
+ response.json(closeIssueWithGitHubPlugin(requestRepositoryRoot(request, options, controllerHome), request.params.issueId));
3027
+ } catch (error) {
3028
+ response.status(400).json({ error: errorMessage(error) });
3029
+ }
3030
+ });
3031
+ app.get("/api/jobs/:jobId", (request, response) => {
3032
+ try {
3033
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
3034
+ response.json({
3035
+ job: getLocalBridgeJob(repoRoot, request.params.jobId),
3036
+ events: getLocalBridgeJobEvents(repoRoot, request.params.jobId),
3037
+ });
3038
+ } catch (error) {
3039
+ response.status(404).json({ error: errorMessage(error) });
3040
+ }
3041
+ });
3042
+ app.post("/api/jobs", (_request, response) => {
3043
+ response.status(410).json(localBridgeExecutionRetiredPayload());
3044
+ });
3045
+ app.post("/api/jobs/:jobId/cancel", (request, response) => {
3046
+ try {
3047
+ response.json(
3048
+ cancelLocalBridgeJob(requestRepositoryRoot(request, options, controllerHome), request.params.jobId),
3049
+ );
3050
+ } catch (error) {
3051
+ response.status(400).json({ error: errorMessage(error) });
3052
+ }
3053
+ });
3054
+ app.get("/api/runs/:runId", (request, response) => {
3055
+ try {
3056
+ response.json(getAgentJob(requestRepositoryRoot(request, options, controllerHome), request.params.runId));
3057
+ } catch (error) {
3058
+ response.status(404).json({ error: errorMessage(error) });
3059
+ }
3060
+ });
3061
+ app.get("/api/runs/:runId/log", (request, response) => {
3062
+ try {
3063
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
3064
+ const run = getAgentJob(repoRoot, request.params.runId);
3065
+ const result = getAgentJobLog(
3066
+ repoRoot,
3067
+ request.params.runId,
3068
+ false,
3069
+ );
3070
+ response.json({ ...result, status: run.status, agent: run.agent });
3071
+ } catch (error) {
3072
+ response.status(404).json({ error: errorMessage(error) });
3073
+ }
3074
+ });
3075
+ app.get("/api/runs/:runId/events", (request, response) => {
3076
+ try {
3077
+ response.json({
3078
+ events: getAgentJobEvents(requestRepositoryRoot(request, options, controllerHome), request.params.runId, 500),
3079
+ });
3080
+ } catch (error) {
3081
+ response.status(404).json({ error: errorMessage(error) });
3082
+ }
3083
+ });
3084
+ app.post("/api/runs/:runId/finish", (request, response) => {
3085
+ try {
3086
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
3087
+ const result = finishTaskRun(repoRoot, {
3088
+ runId: request.params.runId,
3089
+ decision: String(request.body?.decision ?? "auto").replace(/-/g, "_") as never,
3090
+ reviewer: queryString(request.body?.reviewer) ?? "local-bridge-completion",
3091
+ note: queryString(request.body?.note),
3092
+ cleanup: request.body?.keepWorktree !== true,
3093
+ commit: request.body?.commit !== false,
3094
+ });
3095
+ localSnapshotCache.delete(repoRoot);
3096
+ response.json(result);
3097
+ } catch (error) {
3098
+ response.status(400).json({ error: errorMessage(error) });
3099
+ }
3100
+ });
3101
+ app.get("/api/runs/:runId/diff", (request, response) => {
3102
+ try {
3103
+ response.json(taskRunDiff(requestRepositoryRoot(request, options, controllerHome), request.params.runId));
3104
+ } catch (error) {
3105
+ response.status(400).json({ error: errorMessage(error) });
3106
+ }
3107
+ });
3108
+ app.post("/api/runs/:runId/integrate", (request, response) => {
3109
+ try {
3110
+ const repoRoot = requestRepositoryRoot(request, options, controllerHome);
3111
+ const integrated = integrateAgentJob(
3112
+ repoRoot,
3113
+ getMcpPolicy("controller", { repoRoot }),
3114
+ request.params.runId,
3115
+ );
3116
+ const cleanup = cleanupIntegratedWorktree(
3117
+ repoRoot,
3118
+ request.params.runId,
3119
+ );
3120
+ response.json({
3121
+ integrated,
3122
+ cleanup,
3123
+ run: getAgentJob(repoRoot, request.params.runId),
3124
+ });
3125
+ } catch (error) {
3126
+ response.status(400).json({ error: errorMessage(error) });
3127
+ }
3128
+ });
3129
+ app.post("/api/runs/:runId/cancel", (request, response) => {
3130
+ try {
3131
+ response.json(cancelAgentJob(requestRepositoryRoot(request, options, controllerHome), request.params.runId));
3132
+ } catch (error) {
3133
+ response.status(400).json({ error: errorMessage(error) });
3134
+ }
3135
+ });
3136
+ app.post("/api/runs/:runId/retry", (_request, response) => {
3137
+ // Fail closed: Kernel-managed Agent Run retry is retired. Do not probe
3138
+ // providers, mutate Task state, or create Runs/Jobs from this route.
3139
+ response.status(410).json({
3140
+ error: "AGENT_RUN_RETIRED",
3141
+ message: "Kernel-managed Agent Run retry is disabled. Create or claim a WorkContract and start an external SuperController through Thin Launcher.",
3142
+ migration: {
3143
+ path: ["WorkContract", "controller_claim", "Process Runtime", "external SuperController"],
3144
+ },
3145
+ });
3146
+ });
3147
+
3148
+ const server = createServer(app);
3149
+ server.listen(requestedPort, host);
3150
+ await new Promise<void>((resolve, reject) => {
3151
+ server.once("listening", resolve);
3152
+ server.once("error", reject);
3153
+ });
3154
+ server.keepAliveTimeout = 65_000;
3155
+ server.headersTimeout = 70_000;
3156
+ server.requestTimeout = 120_000;
3157
+ server.on("close", () => {
3158
+ clearInterval(streamInterval);
3159
+ for (const client of streamClients.keys()) client.end();
3160
+ streamClients.clear();
3161
+ localSnapshotCache.delete(repoRoot);
3162
+ });
3163
+ const address = server.address();
3164
+ const port =
3165
+ typeof address === "object" && address ? address.port : requestedPort;
3166
+ const url = `http://${host === "::1" ? "[::1]" : host}:${port}/`;
3167
+ if (options.openBrowser) openUrl(url);
3168
+ return {
3169
+ host,
3170
+ port,
3171
+ url,
3172
+ token,
3173
+ server,
3174
+ close: () =>
3175
+ new Promise<void>((resolve, reject) => {
3176
+ if (!server.listening) {
3177
+ resolve();
3178
+ return;
3179
+ }
3180
+ server.close((error) => (error ? reject(error) : resolve()));
3181
+ }),
3182
+ };
3183
+ }