@moretea-labs/forge 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +46 -0
  3. package/CLAUDE.md +2 -0
  4. package/DESIGN.md +7 -2
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +2 -2
  7. package/SKILL.md +2 -2
  8. package/THIRD_PARTY_NOTICES.md +1 -1
  9. package/agents/openai.yaml +1 -1
  10. package/assets/hooks/hook-input.sh +11 -8
  11. package/assets/hooks/lib/session-state.sh +10 -7
  12. package/assets/hooks/lib/workflow-state.sh +13 -177
  13. package/assets/hooks/post-tool-observer.sh +8 -2
  14. package/assets/hooks/pre-edit-guard.sh +8 -39
  15. package/assets/hooks/prompt-guard.sh +7 -83
  16. package/assets/hooks/session-start-context.sh +56 -1
  17. package/assets/partials/05-workflow.partial.md +1 -1
  18. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  19. package/assets/plugin-registry.v1.json +54 -3
  20. package/assets/reference-configs/document-generation.md +1 -1
  21. package/assets/reference-configs/harness-overview.md +1 -1
  22. package/assets/reference-configs/hook-operations.md +1 -1
  23. package/assets/reference-configs/sprint-contracts.md +3 -3
  24. package/assets/skill-version.json +2 -2
  25. package/assets/templates/contract.template.md +2 -7
  26. package/assets/templates/helpers/capture-plan.sh +2 -2
  27. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  28. package/assets/templates/helpers/contract-worktree.sh +1 -21
  29. package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/new-plan.sh +1 -1
  32. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  33. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  34. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/plan.template.md +1 -1
  38. package/assets/templates/review.template.md +9 -34
  39. package/assets/workflow-contract.v1.json +10 -19
  40. package/docs/README.md +10 -10
  41. package/docs/architecture/CURRENT.md +170 -0
  42. package/docs/architecture/EVOLUTION.md +42 -0
  43. package/docs/architecture/current/README.md +4 -156
  44. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  45. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  46. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  47. package/docs/architecture/history/README.md +7 -0
  48. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  49. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  50. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  51. package/docs/architecture/history.md +3 -34
  52. package/docs/architecture/index.md +15 -98
  53. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  54. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  55. package/docs/architecture/versions/1.6.md +38 -0
  56. package/docs/architecture/versions/1.7.md +41 -0
  57. package/docs/forge-chatgpt-browser-engine.md +2 -2
  58. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  59. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  60. package/docs/forge-github-issue-launcher.md +1 -1
  61. package/docs/forge-runtime-self-healing-loop.md +1 -1
  62. package/docs/operations/controller-browser-plugin.md +38 -22
  63. package/docs/operations/controller-performance-and-502.md +124 -0
  64. package/docs/operations/controller-reliability-runbook.md +1 -1
  65. package/docs/operations/features.md +3 -3
  66. package/docs/operations/github-repository.md +1 -1
  67. package/docs/operations/homebrew.md +2 -2
  68. package/docs/operations/mcp-tool-exposure.md +5 -3
  69. package/docs/tutorials/01-install-and-start.md +1 -1
  70. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Installation.md +1 -1
  74. package/docs/wiki/Integrations.md +2 -2
  75. package/docs/wiki/Quick-Start.md +1 -1
  76. package/docs/wiki/Work-Lifecycle.md +1 -1
  77. package/package.json +29 -10
  78. package/scripts/activate-source-baseline.ts +17 -13
  79. package/scripts/benchmark-background-check-overlap.ts +228 -0
  80. package/scripts/benchmark-mcp-host.sh +117 -0
  81. package/scripts/benchmark-mcp-transport.ts +170 -0
  82. package/scripts/benchmark-quality-harness.ts +146 -0
  83. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  84. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  85. package/scripts/benchmark-typescript-navigation.ts +186 -0
  86. package/scripts/benchmark-work-efficiency.ts +334 -0
  87. package/scripts/capture-plan.sh +3 -3
  88. package/scripts/check-agent-tooling.sh +8 -35
  89. package/scripts/check-architecture-sync.sh +17 -86
  90. package/scripts/check-mcp-compatibility.ts +14 -0
  91. package/scripts/check-public-docs.sh +14 -1
  92. package/scripts/check-runtime-architecture.mjs +107 -64
  93. package/scripts/check-stable-baseline.ts +92 -0
  94. package/scripts/check-task-workflow.sh +4 -4
  95. package/scripts/contract-worktree.sh +1 -21
  96. package/scripts/ensure-task-workflow.sh +22 -55
  97. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  98. package/scripts/install-standalone-recovery.ts +24 -0
  99. package/scripts/lib/project-init-lib.sh +15 -44
  100. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  101. package/scripts/migrate-project-template.sh +23 -1179
  102. package/scripts/new-plan.sh +1 -1
  103. package/scripts/plan-to-todo.sh +11 -36
  104. package/scripts/run-governed-gate.ts +2 -1
  105. package/scripts/ship-worktrees.sh +2 -29
  106. package/scripts/smoke-ios-physical-device.ts +4 -2
  107. package/scripts/smoke-runtime-recovery.ts +1 -35
  108. package/scripts/smoke-schedule-engine.ts +1 -20
  109. package/scripts/sprint-backlog.sh +1 -1
  110. package/scripts/verify-contract.sh +1 -111
  111. package/scripts/verify-forge-runtime.sh +0 -1
  112. package/scripts/verify-sprint.sh +3 -180
  113. package/scripts/workflow-contract.ts +1 -5
  114. package/src/cli/commands/chatgpt.ts +31 -2
  115. package/src/cli/commands/init-hook.ts +3 -3
  116. package/src/cli/commands/mcp.ts +9 -5
  117. package/src/cli/commands/plugin.ts +37 -10
  118. package/src/cli/commands/recovery.ts +25 -3
  119. package/src/cli/commands/runtime.ts +55 -3
  120. package/src/cli/commands/security.ts +12 -1
  121. package/src/cli/commands/setup-profile.ts +31 -21
  122. package/src/cli/controller/check-runner.ts +106 -3
  123. package/src/cli/controller/composite-result.ts +1 -1
  124. package/src/cli/controller/context/focus.ts +51 -0
  125. package/src/cli/controller/context/instruction-context.ts +114 -0
  126. package/src/cli/controller/context/known-paths.ts +76 -0
  127. package/src/cli/controller/context/query-planning.ts +110 -0
  128. package/src/cli/controller/context/rule-resolution.ts +532 -0
  129. package/src/cli/controller/context/source-materializer.ts +278 -0
  130. package/src/cli/controller/context/structural.ts +16 -0
  131. package/src/cli/controller/context/types.ts +286 -0
  132. package/src/cli/controller/context-pack.ts +898 -430
  133. package/src/cli/controller/launch-agents.ts +77 -2
  134. package/src/cli/controller/operational-plan.ts +2 -2
  135. package/src/cli/controller/work-mode.ts +6 -4
  136. package/src/cli/editing/edit-session.ts +60 -7
  137. package/src/cli/github/github.ts +133 -0
  138. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  139. package/src/cli/hook/prompt-intents.ts +1 -1
  140. package/src/cli/installer/managed-entries.ts +15 -1
  141. package/src/cli/local-bridge/console-automations.ts +225 -67
  142. package/src/cli/local-bridge/console-view-models.ts +3 -35
  143. package/src/cli/local-bridge/facade-api.ts +2 -217
  144. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  145. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
  146. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  147. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
  148. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  149. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  150. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
  151. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  152. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  153. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
  154. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  155. package/src/cli/local-bridge/frontend/src/types.ts +8 -7
  156. package/src/cli/local-bridge/job-store.ts +1 -490
  157. package/src/cli/local-bridge/server.ts +29 -532
  158. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  159. package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
  160. package/src/cli/mcp/auth.ts +5 -5
  161. package/src/cli/mcp/instructions.ts +3 -2
  162. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  163. package/src/cli/mcp/multi-repository.ts +16 -5
  164. package/src/cli/mcp/oauth.ts +175 -26
  165. package/src/cli/mcp/paths.ts +8 -2
  166. package/src/cli/mcp/repository-tools.ts +356 -101
  167. package/src/cli/mcp/server.ts +625 -54
  168. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  169. package/src/cli/mcp/toolset-selection.ts +1 -1
  170. package/src/cli/mcp/toolset.ts +42 -5
  171. package/src/cli/mcp/transports/http.ts +125 -15
  172. package/src/cli/mcp/transports/session-registry.ts +67 -5
  173. package/src/cli/mcp/types.ts +3 -3
  174. package/src/cli/repositories/command-classifier.ts +209 -14
  175. package/src/cli/repositories/command-executor.ts +55 -521
  176. package/src/cli/repositories/command-process.ts +108 -0
  177. package/src/cli/repositories/command-scope.ts +99 -41
  178. package/src/cli/repositories/controller-home.ts +7 -8
  179. package/src/cli/repositories/git-command-executor.ts +2 -0
  180. package/src/cli/repositories/locks.ts +53 -16
  181. package/src/cli/repositories/repository-snapshot.ts +121 -0
  182. package/src/cli/repositories/safe-patch.ts +6 -4
  183. package/src/cli/repositories/selected-path-actions.ts +17 -9
  184. package/src/cli/repositories/structured-git.ts +110 -10
  185. package/src/cli/repository/inspector.ts +565 -62
  186. package/src/cli/repository/session-cache.ts +58 -17
  187. package/src/cli/v81-entry.ts +1 -4
  188. package/src/effects/fs-transaction.ts +85 -17
  189. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  190. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  191. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  192. package/src/runtime/context/lsp-client.ts +283 -0
  193. package/src/runtime/context/semantic-navigation.ts +275 -0
  194. package/src/runtime/context/swift-navigation.ts +565 -0
  195. package/src/runtime/context/typescript-navigation.ts +226 -0
  196. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  197. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
  198. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  199. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  200. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  201. package/src/runtime/control-plane/execution/validation.ts +15 -2
  202. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  203. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  204. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  205. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  206. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  207. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
  208. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
  209. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
  210. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  211. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  212. package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
  213. package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
  214. package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
  215. package/src/runtime/control-plane/facade/index.ts +1 -0
  216. package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
  217. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  218. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  219. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  220. package/src/runtime/control-plane/facade/types.ts +55 -11
  221. package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
  222. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  223. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  224. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  225. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  226. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  227. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  228. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  229. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  230. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  231. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  232. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  233. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  234. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  235. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  236. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  237. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  238. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  239. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  240. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  241. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
  242. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  243. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  244. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  245. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  246. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  247. package/src/runtime/control-plane/release-retention.ts +62 -2
  248. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  249. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  250. package/src/runtime/diagnostics/mcp-timing.ts +19 -0
  251. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  252. package/src/runtime/diagnostics/performance.ts +24 -1
  253. package/src/runtime/evidence/event-ledger.ts +1 -1
  254. package/src/runtime/execution/jobs/store.ts +58 -4
  255. package/src/runtime/execution/jobs/types.ts +1 -1
  256. package/src/runtime/execution/managed-workspace.ts +203 -1
  257. package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
  258. package/src/runtime/execution/process-runtime/check-result.ts +63 -2
  259. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  260. package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
  261. package/src/runtime/execution/process-runtime/gc.ts +98 -12
  262. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  263. package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
  264. package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
  265. package/src/runtime/execution/process-runtime/runtime.ts +59 -4
  266. package/src/runtime/execution/process-runtime/store.ts +7 -0
  267. package/src/runtime/execution/process-runtime/types.ts +2 -0
  268. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  269. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  270. package/src/runtime/execution/workers/executor.ts +0 -48
  271. package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
  272. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  273. package/src/runtime/gateway/mcp/router.ts +185 -83
  274. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  275. package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
  276. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  277. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  278. package/src/runtime/plugins/browser-adapter.ts +2925 -318
  279. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  280. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  281. package/src/runtime/plugins/browser-handoff.ts +92 -5
  282. package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
  283. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  284. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  285. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  286. package/src/runtime/plugins/browser-runtime.ts +183 -0
  287. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  288. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  289. package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
  290. package/src/runtime/plugins/external-adapter.ts +324 -3
  291. package/src/runtime/plugins/external-registration.ts +84 -5
  292. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  293. package/src/runtime/plugins/first-party-registry.ts +4 -2
  294. package/src/runtime/plugins/github-adapter.ts +125 -0
  295. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  296. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  297. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  298. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  299. package/src/runtime/plugins/ios-adapter.ts +106 -16
  300. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  301. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  302. package/src/runtime/plugins/lightweight-action.ts +153 -0
  303. package/src/runtime/plugins/local-system-adapter.ts +125 -8
  304. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  305. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  306. package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
  307. package/src/runtime/plugins/resend-adapter.ts +1 -1
  308. package/src/runtime/plugins/store.ts +366 -24
  309. package/src/runtime/plugins/types.ts +39 -2
  310. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  311. package/src/runtime/projections/git-status-sampler.ts +16 -16
  312. package/src/runtime/projections/materialized-view.ts +28 -10
  313. package/src/runtime/recovery/actions.ts +0 -10
  314. package/src/runtime/recovery/classifier.ts +0 -9
  315. package/src/runtime/recovery/maintenance-executor.ts +415 -18
  316. package/src/runtime/recovery/probe.ts +0 -1
  317. package/src/runtime/recovery/types.ts +0 -1
  318. package/src/runtime/root/mcp-transport.ts +180 -10
  319. package/src/runtime/root/package-connector-service.ts +186 -8
  320. package/src/runtime/root/package-runtime-release.ts +143 -17
  321. package/src/runtime/root/package-runtime-service.ts +430 -22
  322. package/src/runtime/root/release-manifest.ts +7 -2
  323. package/src/runtime/root/release-materialize.ts +162 -19
  324. package/src/runtime/root/release-store.ts +48 -11
  325. package/src/runtime/root/runtime.ts +23 -5
  326. package/src/runtime/root/service.ts +100 -27
  327. package/src/runtime/root/types.ts +2 -0
  328. package/src/runtime/safe-tooling/index.ts +0 -1
  329. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  330. package/src/runtime/safe-tooling/types.ts +0 -30
  331. package/src/runtime/shared/trusted-executable.ts +62 -0
  332. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  333. package/src/runtime/standalone-recovery/core.ts +702 -94
  334. package/src/runtime/standalone-recovery/entry.ts +99 -23
  335. package/src/runtime/standalone-recovery/installer.ts +19 -3
  336. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  337. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  338. package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
  339. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  340. package/src/runtime/workflow/schedules/engine.ts +509 -80
  341. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  342. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  343. package/src/runtime/workflow/schedules/store.ts +131 -10
  344. package/src/runtime/workflow/schedules/types.ts +6 -3
  345. package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
  346. package/src/testing/test-governance.ts +8 -40
  347. package/assets/reference-configs/heartbeat-triage.md +0 -57
  348. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  349. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  350. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  351. package/bin/forge-desktop-helper.mjs +0 -204
  352. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  353. package/docs/architecture/current/architecture-invariants.md +0 -331
  354. package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
  355. package/docs/architecture/current/codegraph-context-provider.md +0 -226
  356. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  357. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  358. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
  359. package/docs/architecture/current/entity-model.md +0 -592
  360. package/docs/architecture/current/external-plugin-governance.md +0 -396
  361. package/docs/architecture/current/failure-recovery.md +0 -447
  362. package/docs/architecture/current/governance.md +0 -213
  363. package/docs/architecture/current/human-interaction-plane.md +0 -87
  364. package/docs/architecture/current/implementation-status.md +0 -128
  365. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  366. package/docs/architecture/current/legacy-governance.md +0 -58
  367. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  368. package/docs/architecture/current/migration-roadmap.md +0 -105
  369. package/docs/architecture/current/multi-repository-execution.md +0 -371
  370. package/docs/architecture/current/operations-runbook.md +0 -45
  371. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  372. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  373. package/docs/architecture/current/repository-access-modes.md +0 -102
  374. package/docs/architecture/current/route-policy.md +0 -15
  375. package/docs/architecture/current/route-session-concurrency.md +0 -85
  376. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  377. package/docs/architecture/current/runtime-directory-map.md +0 -49
  378. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  379. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  380. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  381. package/docs/architecture/current/system-overview.md +0 -144
  382. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  383. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  384. package/docs/architecture/current/thin-harness-v1.md +0 -311
  385. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  386. package/docs/forge-autonomous-goal-loop.md +0 -9
  387. package/docs/forge-provider-configuration.md +0 -7
  388. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
  389. package/docs/operations/controller-desktop-plugin.md +0 -112
  390. package/docs/operations/gmail-assistant-routines.md +0 -186
  391. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  392. package/docs/operations/provider-configuration.md +0 -31
  393. package/scripts/harness-trace-grade.sh +0 -150
  394. package/scripts/heartbeat-triage.sh +0 -361
  395. package/scripts/maintenance-triage.sh +0 -167
  396. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  397. package/src/cli/repositories/goal-registry.ts +0 -239
  398. package/src/runtime/assistant/action-proposals.ts +0 -279
  399. package/src/runtime/assistant/intent.ts +0 -395
  400. package/src/runtime/assistant/model-provider.ts +0 -460
  401. package/src/runtime/assistant/openapi.ts +0 -223
  402. package/src/runtime/assistant/policy.ts +0 -89
  403. package/src/runtime/assistant/readiness.ts +0 -258
  404. package/src/runtime/assistant/routine-runtime.ts +0 -477
  405. package/src/runtime/assistant/schedule-binding.ts +0 -171
  406. package/src/runtime/assistant/standing-grants.ts +0 -375
  407. package/src/runtime/assistant/store.ts +0 -243
  408. package/src/runtime/assistant/types.ts +0 -135
  409. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  410. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  411. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  412. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  413. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  414. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  415. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  416. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  417. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  418. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  419. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  420. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  421. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  422. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  423. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  424. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  425. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  426. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  427. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  428. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  429. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
  430. package/src/runtime/workflow/findings/store.ts +0 -204
  431. package/src/runtime/workflow/findings/types.ts +0 -58
  432. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  433. package/src/runtime/workflow/portfolio/store.ts +0 -185
  434. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -14,6 +14,15 @@ import { CONTROL_PLANE_SCHEMA_VERSION } from '../control-plane/persistence/sqlit
14
14
  * `forge runtime service install` operation; staging alone never starts or
15
15
  * publishes anything.
16
16
  */
17
+ export const FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER = 'com.moretea.forge.runtime';
18
+
19
+ export interface MacOSRuntimeCodeSigning {
20
+ identifier: string;
21
+ teamIdentifier: string;
22
+ designatedRequirement: string;
23
+ authority: string;
24
+ }
25
+
17
26
  export interface StagedRuntimeRelease {
18
27
  releasePath: string;
19
28
  manifestPath: string;
@@ -22,20 +31,23 @@ export interface StagedRuntimeRelease {
22
31
  diagnosticArtifactIdentity?: string;
23
32
  browserNodeBridgeArtifactIdentity?: string;
24
33
  browserHandoffArtifactIdentity?: string;
25
- desktopHelperArtifactIdentity?: string;
26
34
  processRunnerArtifactIdentity?: string;
27
35
  checkRunnerArtifactIdentity?: string;
36
+ pluginActionSidecarArtifactIdentity?: string;
28
37
  externalPluginProbeArtifactIdentity?: string;
29
38
  codeGraphNodeArtifactIdentity?: string;
30
39
  codeGraphSidecarArtifactIdentity?: string;
31
40
  codeGraphLibraryArtifactIdentity?: string;
32
41
  controllerUiArtifactIdentity?: string;
42
+ macosCodeSigning?: MacOSRuntimeCodeSigning;
33
43
  manifestSha256: string;
34
44
  sourceCommit: string;
35
45
  }
36
46
 
37
47
  export interface RuntimeReleaseMaterializerDependencies {
38
48
  now?: () => number;
49
+ platform?: NodeJS.Platform;
50
+ signMacOSRuntime?: (input: { executable: string; controllerHome: string }) => MacOSRuntimeCodeSigning;
39
51
  uuid?: () => string;
40
52
  compileBinary?: (input: { sourceRoot: string; outputPath: string; entryPath?: string }) => { ok: boolean; stderr?: string; stdout?: string; error?: string };
41
53
  bundleNodeHost?: (input: { sourceRoot: string; outputPath: string; entryPath: string }) => { ok: boolean; stderr?: string; stdout?: string; error?: string };
@@ -59,6 +71,8 @@ export interface CandidateRuntimeStageReceiptV1 {
59
71
  }
60
72
 
61
73
  export interface CandidateRuntimeReleaseStagerDependencies {
74
+ platform?: NodeJS.Platform;
75
+ inspectMacOSRuntime?: (executable: string) => MacOSRuntimeCodeSigning;
62
76
  runCandidateStager?: (input: {
63
77
  bunExecutable: string;
64
78
  scriptPath: string;
@@ -78,6 +92,95 @@ function sha256(path: string): string {
78
92
  return createHash('sha256').update(readFileSync(path)).digest('hex');
79
93
  }
80
94
 
95
+ function parseMacOSRuntimeCodeSigning(value: unknown, context: string): MacOSRuntimeCodeSigning | undefined {
96
+ if (value === undefined) return undefined;
97
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${context}: macosCodeSigning must be an object`);
98
+ const record = value as Record<string, unknown>;
99
+ const field = (name: keyof MacOSRuntimeCodeSigning): string => {
100
+ const entry = record[name];
101
+ if (typeof entry !== 'string' || !entry.trim()) throw new Error(`${context}: macosCodeSigning.${name} is required`);
102
+ return entry.trim();
103
+ };
104
+ const signing = {
105
+ identifier: field('identifier'),
106
+ teamIdentifier: field('teamIdentifier'),
107
+ designatedRequirement: field('designatedRequirement'),
108
+ authority: field('authority'),
109
+ };
110
+ if (signing.identifier !== FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER) {
111
+ throw new Error(`${context}: macosCodeSigning.identifier must be ${FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER}`);
112
+ }
113
+ if (!/^[A-Z0-9]{10}$/.test(signing.teamIdentifier)) throw new Error(`${context}: macosCodeSigning.teamIdentifier is invalid`);
114
+ if (!signing.authority.startsWith('Developer ID Application: ')) throw new Error(`${context}: macosCodeSigning.authority must be Developer ID Application`);
115
+ if (!signing.designatedRequirement.includes(`identifier \"${FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER}\"`)
116
+ || !signing.designatedRequirement.includes('anchor apple generic')
117
+ || !signing.designatedRequirement.includes(`certificate leaf[subject.OU] = ${signing.teamIdentifier}`)) {
118
+ throw new Error(`${context}: macosCodeSigning.designatedRequirement is not the stable Developer ID contract`);
119
+ }
120
+ return signing;
121
+ }
122
+
123
+ function readActiveMacOSRuntimeTeam(controllerHome: string): string | undefined {
124
+ const authorityPath = join(resolve(controllerHome), 'runtime', 'releases', 'authority.json');
125
+ if (!existsSync(authorityPath)) return undefined;
126
+ try {
127
+ const authority = JSON.parse(readFileSync(authorityPath, 'utf8')) as { active?: { manifestPath?: string } };
128
+ if (!authority.active?.manifestPath || !existsSync(authority.active.manifestPath)) return undefined;
129
+ const manifest = JSON.parse(readFileSync(authority.active.manifestPath, 'utf8')) as Record<string, unknown>;
130
+ return parseMacOSRuntimeCodeSigning(manifest.macosCodeSigning, 'RUNTIME_RELEASE_ACTIVE_SIGNING_INVALID')?.teamIdentifier;
131
+ } catch (error) {
132
+ if (error instanceof Error && error.message.startsWith('RUNTIME_RELEASE_ACTIVE_SIGNING_INVALID')) throw error;
133
+ return undefined;
134
+ }
135
+ }
136
+
137
+ function inspectMacOSRuntimeCodeSigning(executable: string): MacOSRuntimeCodeSigning {
138
+ const verify = runProcess('codesign', ['--verify', '--strict', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
139
+ if (!verify.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNATURE_INVALID: ${verify.stderr || verify.stdout || verify.error}`.slice(0, 2_000));
140
+ const detail = runProcess('codesign', ['-dv', '--verbose=4', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
141
+ if (!detail.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNATURE_INSPECTION_FAILED: ${detail.stderr || detail.stdout || detail.error}`.slice(0, 2_000));
142
+ const detailText = `${detail.stdout}\n${detail.stderr}`;
143
+ const identifier = detailText.match(/^Identifier=(.+)$/m)?.[1]?.trim() ?? '';
144
+ const teamIdentifier = detailText.match(/^TeamIdentifier=(.+)$/m)?.[1]?.trim() ?? '';
145
+ const authority = detailText.match(/^Authority=(Developer ID Application:.+)$/m)?.[1]?.trim() ?? '';
146
+ const requirementResult = runProcess('codesign', ['-d', '-r-', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
147
+ if (!requirementResult.ok) throw new Error(`RUNTIME_RELEASE_MACOS_REQUIREMENT_INSPECTION_FAILED: ${requirementResult.stderr || requirementResult.stdout || requirementResult.error}`.slice(0, 2_000));
148
+ const requirementText = `${requirementResult.stdout}\n${requirementResult.stderr}`;
149
+ const designatedRequirement = requirementText.match(/^designated => (.+)$/m)?.[1]?.trim() ?? '';
150
+ return parseMacOSRuntimeCodeSigning({ identifier, teamIdentifier, authority, designatedRequirement }, 'RUNTIME_RELEASE_MACOS_SIGNATURE_INVALID')!;
151
+ }
152
+
153
+ function defaultSignMacOSRuntime(input: { executable: string; controllerHome: string }): MacOSRuntimeCodeSigning {
154
+ const identities = runProcess('security', ['find-identity', '-v', '-p', 'codesigning'], { timeoutMs: 30_000, maxOutputBytes: 128 * 1024 });
155
+ if (!identities.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_LOOKUP_FAILED: ${identities.stderr || identities.stdout || identities.error}`.slice(0, 2_000));
156
+ const candidates = identities.stdout.split(/\r?\n/).flatMap((line) => {
157
+ const match = line.match(/^\s*\d+\)\s+([A-Fa-f0-9]{40})\s+\"(Developer ID Application: .+ \(([A-Z0-9]{10})\))\"/);
158
+ return match ? [{ hash: match[1]!, label: match[2]!, teamIdentifier: match[3]! }] : [];
159
+ });
160
+ const configured = process.env.FORGE_MACOS_RUNTIME_SIGNING_IDENTITY?.trim();
161
+ const expectedTeam = readActiveMacOSRuntimeTeam(input.controllerHome);
162
+ let selected = configured
163
+ ? candidates.find((candidate) => candidate.hash.toLowerCase() === configured.toLowerCase() || candidate.label === configured)
164
+ : undefined;
165
+ if (configured && !selected) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_NOT_FOUND');
166
+ if (!selected) {
167
+ const eligible = expectedTeam ? candidates.filter((candidate) => candidate.teamIdentifier === expectedTeam) : candidates;
168
+ const teams = Array.from(new Set(eligible.map((candidate) => candidate.teamIdentifier)));
169
+ if (teams.length !== 1 || eligible.length === 0) {
170
+ throw new Error(expectedTeam ? 'RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_FOR_TEAM_REQUIRED' : 'RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_REQUIRED');
171
+ }
172
+ selected = eligible[0];
173
+ }
174
+ if (expectedTeam && selected.teamIdentifier !== expectedTeam) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_TEAM_CHANGED');
175
+ const signed = runProcess('codesign', [
176
+ '--force', '--sign', selected.hash, '--identifier', FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER, '--options', 'runtime', input.executable,
177
+ ], { timeoutMs: 120_000, maxOutputBytes: 128 * 1024 });
178
+ if (!signed.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNING_FAILED: ${signed.stderr || signed.stdout || signed.error}`.slice(0, 2_000));
179
+ const inspected = inspectMacOSRuntimeCodeSigning(input.executable);
180
+ if (inspected.teamIdentifier !== selected.teamIdentifier) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_TEAM_MISMATCH');
181
+ return inspected;
182
+ }
183
+
81
184
  function sha256Directory(root: string): string {
82
185
  const hash = createHash('sha256');
83
186
  const visit = (directory: string): void => {
@@ -203,12 +306,28 @@ export function stageRuntimeReleaseFromCandidateSource(input: {
203
306
  if (sha256(manifestPath) !== receipt.manifestSha256) {
204
307
  throw new Error('RUNTIME_RELEASE_CANDIDATE_MANIFEST_IDENTITY_MISMATCH');
205
308
  }
309
+ let rawManifest: Record<string, unknown>;
310
+ try { rawManifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>; }
311
+ catch (error) { throw new Error(`RUNTIME_RELEASE_CANDIDATE_MANIFEST_INVALID: ${error instanceof Error ? error.message : String(error)}`); }
312
+ const platform = dependencies.platform ?? process.platform;
313
+ const macosCodeSigning = parseMacOSRuntimeCodeSigning(rawManifest.macosCodeSigning, 'RUNTIME_RELEASE_CANDIDATE_SIGNING_INVALID');
314
+ if (platform === 'darwin') {
315
+ if (!macosCodeSigning) throw new Error('RUNTIME_RELEASE_CANDIDATE_MACOS_SIGNING_REQUIRED');
316
+ const actual = (dependencies.inspectMacOSRuntime ?? inspectMacOSRuntimeCodeSigning)(runtimePath);
317
+ if (actual.identifier !== macosCodeSigning.identifier
318
+ || actual.teamIdentifier !== macosCodeSigning.teamIdentifier
319
+ || actual.authority !== macosCodeSigning.authority
320
+ || actual.designatedRequirement !== macosCodeSigning.designatedRequirement) {
321
+ throw new Error('RUNTIME_RELEASE_CANDIDATE_MACOS_SIGNING_MISMATCH');
322
+ }
323
+ }
206
324
 
207
325
  return {
208
326
  releasePath,
209
327
  manifestPath,
210
328
  releaseId: receipt.releaseId,
211
329
  artifactIdentity: receipt.artifactIdentity,
330
+ ...(macosCodeSigning ? { macosCodeSigning } : {}),
212
331
  manifestSha256: receipt.manifestSha256,
213
332
  sourceCommit: receipt.sourceCommit,
214
333
  };
@@ -297,6 +416,14 @@ export function stageRuntimeRelease(input: {
297
416
  throw new Error(`RUNTIME_RELEASE_BUILD_FAILED: ${compile.stderr || compile.stdout || compile.error}`.slice(0, 2_000));
298
417
  }
299
418
  chmodSync(executable, 0o700);
419
+ const platform = dependencies.platform ?? process.platform;
420
+ const macosCodeSigning = platform === 'darwin'
421
+ ? (dependencies.signMacOSRuntime ?? defaultSignMacOSRuntime)({ executable, controllerHome: resolve(input.controllerHome) })
422
+ : undefined;
423
+ if (platform === 'darwin' && !macosCodeSigning) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_REQUIRED');
424
+ const normalizedMacOSCodeSigning = macosCodeSigning
425
+ ? parseMacOSRuntimeCodeSigning(macosCodeSigning, 'RUNTIME_RELEASE_MACOS_SIGNING_INVALID')
426
+ : undefined;
300
427
  const artifactIdentity = `sha256:${sha256(executable)}`;
301
428
 
302
429
  const diagnosticExecutable = join(staging, 'forge-cli');
@@ -365,6 +492,19 @@ export function stageRuntimeRelease(input: {
365
492
  chmodSync(checkRunnerPath, 0o700);
366
493
  const checkRunnerArtifactIdentity = `sha256:${sha256(checkRunnerPath)}`;
367
494
 
495
+ const pluginActionSidecarEntrypoint = 'forge-plugin-action-sidecar' as const;
496
+ const pluginActionSidecarPath = join(staging, pluginActionSidecarEntrypoint);
497
+ const pluginActionSidecarCompile = compileBinary({
498
+ sourceRoot,
499
+ outputPath: pluginActionSidecarPath,
500
+ entryPath: join(sourceRoot, 'src/runtime/plugins/plugin-action-sidecar.ts'),
501
+ });
502
+ if (!pluginActionSidecarCompile.ok) {
503
+ throw new Error(`RUNTIME_RELEASE_PLUGIN_ACTION_SIDECAR_BUILD_FAILED: ${pluginActionSidecarCompile.stderr || pluginActionSidecarCompile.stdout || pluginActionSidecarCompile.error}`.slice(0, 2_000));
504
+ }
505
+ chmodSync(pluginActionSidecarPath, 0o700);
506
+ const pluginActionSidecarArtifactIdentity = `sha256:${sha256(pluginActionSidecarPath)}`;
507
+
368
508
  const externalPluginProbeEntrypoint = 'external-unix-socket-probe.cjs' as const;
369
509
  const sourceExternalPluginProbePath = join(sourceRoot, 'src', 'runtime', 'plugins', externalPluginProbeEntrypoint);
370
510
  if (!existsSync(sourceExternalPluginProbePath)) {
@@ -407,32 +547,24 @@ export function stageRuntimeRelease(input: {
407
547
  cpSync(sourceControllerUiPath, controllerUiPath, { recursive: true, force: false });
408
548
  const controllerUiArtifactIdentity = `sha256:${sha256Directory(controllerUiPath)}`;
409
549
 
410
- const desktopHelperEntrypoint = 'forge-desktop-helper.mjs' as const;
411
- const sourceDesktopHelperPath = join(sourceRoot, 'bin', desktopHelperEntrypoint);
412
- if (!existsSync(sourceDesktopHelperPath)) {
413
- throw new Error(`RUNTIME_RELEASE_DESKTOP_HELPER_SOURCE_MISSING: ${sourceDesktopHelperPath}`);
414
- }
415
- const desktopHelperPath = join(staging, desktopHelperEntrypoint);
416
- copyFileSync(sourceDesktopHelperPath, desktopHelperPath);
417
- chmodSync(desktopHelperPath, 0o700);
418
- const desktopHelperArtifactIdentity = `sha256:${sha256(desktopHelperPath)}`;
419
550
  const manifest = {
420
551
  schemaVersion: 1,
421
552
  releaseId,
422
553
  artifactIdentity,
423
554
  entrypoint: 'forge-runtime',
555
+ ...(normalizedMacOSCodeSigning ? { macosCodeSigning: normalizedMacOSCodeSigning } : {}),
424
556
  diagnosticEntrypoint: 'forge-cli',
425
557
  diagnosticArtifactIdentity,
426
558
  browserNodeBridgeEntrypoint,
427
559
  browserNodeBridgeArtifactIdentity,
428
560
  browserHandoffEntrypoint,
429
561
  browserHandoffArtifactIdentity,
430
- desktopHelperEntrypoint,
431
- desktopHelperArtifactIdentity,
432
562
  processRunnerEntrypoint,
433
563
  processRunnerArtifactIdentity,
434
564
  checkRunnerEntrypoint,
435
565
  checkRunnerArtifactIdentity,
566
+ pluginActionSidecarEntrypoint,
567
+ pluginActionSidecarArtifactIdentity,
436
568
  externalPluginProbeEntrypoint,
437
569
  externalPluginProbeArtifactIdentity,
438
570
  codeGraphNodeEntrypoint,
@@ -464,10 +596,10 @@ export function stageRuntimeRelease(input: {
464
596
  manifestPath: join(releasePath, 'manifest.json'),
465
597
  releaseId,
466
598
  artifactIdentity,
599
+ ...(normalizedMacOSCodeSigning ? { macosCodeSigning: normalizedMacOSCodeSigning } : {}),
467
600
  diagnosticArtifactIdentity,
468
601
  browserNodeBridgeArtifactIdentity,
469
602
  browserHandoffArtifactIdentity,
470
- desktopHelperArtifactIdentity,
471
603
  processRunnerArtifactIdentity,
472
604
  checkRunnerArtifactIdentity,
473
605
  externalPluginProbeArtifactIdentity,
@@ -484,12 +616,23 @@ export function stageRuntimeRelease(input: {
484
616
  }
485
617
  }
486
618
 
487
- export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
619
+ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease, dependencies: { platform?: NodeJS.Platform; inspectMacOSRuntime?: (executable: string) => MacOSRuntimeCodeSigning } = {}): void {
488
620
  if (!existsSync(release.releasePath) || !existsSync(release.manifestPath)) {
489
621
  throw new Error(`RUNTIME_RELEASE_FILES_MISSING: ${release.releasePath}`);
490
622
  }
491
- if (!existsSync(join(release.releasePath, 'forge-runtime'))) {
492
- throw new Error(`RUNTIME_RELEASE_ENTRYPOINT_MISSING: ${join(release.releasePath, 'forge-runtime')}`);
623
+ const runtimePath = join(release.releasePath, 'forge-runtime');
624
+ if (!existsSync(runtimePath)) {
625
+ throw new Error(`RUNTIME_RELEASE_ENTRYPOINT_MISSING: ${runtimePath}`);
626
+ }
627
+ const platform = dependencies.platform ?? process.platform;
628
+ if (platform === 'darwin' && release.macosCodeSigning) {
629
+ const actual = (dependencies.inspectMacOSRuntime ?? inspectMacOSRuntimeCodeSigning)(runtimePath);
630
+ if (actual.identifier !== release.macosCodeSigning.identifier
631
+ || actual.teamIdentifier !== release.macosCodeSigning.teamIdentifier
632
+ || actual.authority !== release.macosCodeSigning.authority
633
+ || actual.designatedRequirement !== release.macosCodeSigning.designatedRequirement) {
634
+ throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_MISMATCH');
635
+ }
493
636
  }
494
637
  if (release.diagnosticArtifactIdentity && !existsSync(join(release.releasePath, 'forge-cli'))) {
495
638
  throw new Error(`RUNTIME_RELEASE_DIAGNOSTIC_ENTRYPOINT_MISSING: ${join(release.releasePath, 'forge-cli')}`);
@@ -500,15 +643,15 @@ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
500
643
  if (release.browserHandoffArtifactIdentity && !existsSync(join(release.releasePath, 'browser-handoff-host.js'))) {
501
644
  throw new Error(`RUNTIME_RELEASE_BROWSER_HANDOFF_HOST_MISSING: ${join(release.releasePath, 'browser-handoff-host.js')}`);
502
645
  }
503
- if (release.desktopHelperArtifactIdentity && !existsSync(join(release.releasePath, 'forge-desktop-helper.mjs'))) {
504
- throw new Error(`RUNTIME_RELEASE_DESKTOP_HELPER_MISSING: ${join(release.releasePath, 'forge-desktop-helper.mjs')}`);
505
- }
506
646
  if (release.processRunnerArtifactIdentity && !existsSync(join(release.releasePath, 'process-runner.js'))) {
507
647
  throw new Error(`RUNTIME_RELEASE_PROCESS_RUNNER_MISSING: ${join(release.releasePath, 'process-runner.js')}`);
508
648
  }
509
649
  if (release.checkRunnerArtifactIdentity && !existsSync(join(release.releasePath, 'forge-check-runner'))) {
510
650
  throw new Error(`RUNTIME_RELEASE_CHECK_RUNNER_MISSING: ${join(release.releasePath, 'forge-check-runner')}`);
511
651
  }
652
+ if (release.pluginActionSidecarArtifactIdentity && !existsSync(join(release.releasePath, 'forge-plugin-action-sidecar'))) {
653
+ throw new Error(`RUNTIME_RELEASE_PLUGIN_ACTION_SIDECAR_MISSING: ${join(release.releasePath, 'forge-plugin-action-sidecar')}`);
654
+ }
512
655
  if (release.externalPluginProbeArtifactIdentity && !existsSync(join(release.releasePath, 'external-unix-socket-probe.cjs'))) {
513
656
  throw new Error(`RUNTIME_RELEASE_EXTERNAL_PLUGIN_PROBE_MISSING: ${join(release.releasePath, 'external-unix-socket-probe.cjs')}`);
514
657
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'fs';
3
3
  import { dirname, join, resolve } from 'path';
4
4
  import { ensureControllerHome } from '../../cli/repositories/controller-home';
5
5
  import {
@@ -102,9 +102,14 @@ function validRelease(controllerHome: string, release: RuntimePublishedRelease |
102
102
  }
103
103
  }
104
104
 
105
- export function readRuntimeReleaseAuthority(controllerHome: string): RuntimeReleaseAuthority | undefined {
105
+ type RuntimeReleaseAuthorityRead =
106
+ | { state: 'missing' }
107
+ | { state: 'valid'; authority: RuntimeReleaseAuthority }
108
+ | { state: 'invalid'; reason: string };
109
+
110
+ function inspectRuntimeReleaseAuthority(controllerHome: string): RuntimeReleaseAuthorityRead {
106
111
  const path = runtimeReleaseAuthorityPath(controllerHome);
107
- if (!existsSync(path)) return undefined;
112
+ if (!existsSync(path)) return { state: 'missing' };
108
113
  try {
109
114
  const value = JSON.parse(readFileSync(path, 'utf8')) as RuntimeReleaseAuthority;
110
115
  if (
@@ -117,11 +122,24 @@ export function readRuntimeReleaseAuthority(controllerHome: string): RuntimeRele
117
122
  || !Number.isFinite(Date.parse(value.committedAt))
118
123
  || !validRelease(controllerHome, value.active)
119
124
  || (value.previous !== undefined && !validRelease(controllerHome, value.previous))
120
- ) return undefined;
121
- return value;
122
- } catch {
123
- return undefined;
125
+ ) return { state: 'invalid', reason: 'authority fields or referenced release evidence are invalid' };
126
+ return { state: 'valid', authority: value };
127
+ } catch (error) {
128
+ return { state: 'invalid', reason: error instanceof Error ? error.message : String(error) };
129
+ }
130
+ }
131
+
132
+ function mutableRuntimeReleaseAuthority(controllerHome: string): RuntimeReleaseAuthority | undefined {
133
+ const observed = inspectRuntimeReleaseAuthority(controllerHome);
134
+ if (observed.state === 'invalid') {
135
+ throw new Error(`RUNTIME_RELEASE_AUTHORITY_INVALID_EXISTING: ${observed.reason}`);
124
136
  }
137
+ return observed.state === 'valid' ? observed.authority : undefined;
138
+ }
139
+
140
+ export function readRuntimeReleaseAuthority(controllerHome: string): RuntimeReleaseAuthority | undefined {
141
+ const observed = inspectRuntimeReleaseAuthority(controllerHome);
142
+ return observed.state === 'valid' ? observed.authority : undefined;
125
143
  }
126
144
 
127
145
  function writeRuntimeReleaseAuthority(controllerHome: string, authority: RuntimeReleaseAuthority): RuntimeReleaseAuthority {
@@ -145,7 +163,7 @@ export function ensureActiveRuntimeRelease(
145
163
  operationId = 'runtime-start',
146
164
  ): RuntimeReleaseAuthority {
147
165
  const active = manifestRecord(controllerHome, manifestPath);
148
- const current = readRuntimeReleaseAuthority(controllerHome);
166
+ const current = mutableRuntimeReleaseAuthority(controllerHome);
149
167
  if (current) {
150
168
  if (!sameRelease(current.active, active)) throw new Error('RUNTIME_RELEASE_AUTHORITY_MISMATCH');
151
169
  return current;
@@ -169,8 +187,18 @@ export function publishRuntimeRelease(
169
187
  ): RuntimeReleaseAuthority {
170
188
  if (!operationId.trim()) throw new Error('RUNTIME_RELEASE_OPERATION_ID_REQUIRED');
171
189
  const candidate = manifestRecord(controllerHome, manifestPath);
172
- const current = readRuntimeReleaseAuthority(controllerHome);
173
- if (!current) return ensureActiveRuntimeRelease(controllerHome, manifestPath, operationId);
190
+ const current = mutableRuntimeReleaseAuthority(controllerHome);
191
+ if (!current) {
192
+ return writeRuntimeReleaseAuthority(controllerHome, {
193
+ schemaVersion: 1,
194
+ status: 'committed',
195
+ revision: 1,
196
+ fencingToken: randomUUID(),
197
+ active: candidate,
198
+ operationId,
199
+ committedAt: new Date().toISOString(),
200
+ });
201
+ }
174
202
  if (sameRelease(current.active, candidate)) return current;
175
203
  const backup = backupPath(controllerHome, current.active.releaseId, operationId);
176
204
  const inspection = dependencies.backupDatabase(controllerHome, backup);
@@ -190,13 +218,22 @@ export function publishRuntimeRelease(
190
218
  });
191
219
  }
192
220
 
221
+ export function revertInitialRuntimeReleasePublication(controllerHome: string, operationId: string): void {
222
+ const current = mutableRuntimeReleaseAuthority(controllerHome);
223
+ if (!current) return;
224
+ if (current.revision !== 1 || current.previous !== undefined || current.operationId !== operationId) {
225
+ throw new Error('RUNTIME_RELEASE_INITIAL_PUBLICATION_REVERT_MISMATCH');
226
+ }
227
+ rmSync(runtimeReleaseAuthorityPath(controllerHome), { force: true });
228
+ }
229
+
193
230
  export function rollbackRuntimeRelease(
194
231
  controllerHome: string,
195
232
  operationId: string,
196
233
  dependencies: RuntimeReleaseStoreDependencies = DEFAULT_DEPENDENCIES,
197
234
  ): RuntimeReleaseAuthority {
198
235
  if (!operationId.trim()) throw new Error('RUNTIME_RELEASE_OPERATION_ID_REQUIRED');
199
- const current = readRuntimeReleaseAuthority(controllerHome);
236
+ const current = mutableRuntimeReleaseAuthority(controllerHome);
200
237
  const target = current?.previous;
201
238
  if (!current || !target?.databaseBackup) throw new Error('RUNTIME_PREVIOUS_RELEASE_UNAVAILABLE');
202
239
  const currentBackup = backupPath(controllerHome, current.active.releaseId, operationId);
@@ -6,6 +6,8 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
6
6
  import type { ControlPlaneDatabaseInspection } from '../control-plane/persistence/sqlite-store';
7
7
  import { inspectControlPlaneDatabase } from '../control-plane/persistence/sqlite-store';
8
8
  import { activateExclusiveWorkAdmission } from '../control-plane/facade/work-admission-policy';
9
+ import { closeCodeGraphReadProviderSessions } from '../context/codegraph-read-provider';
10
+ import { cancelAllLightweightProcesses } from '../execution/process-runtime/lightweight-managed';
9
11
  import {
10
12
  collectRuntimeSourceIdentity,
11
13
  rotateRuntimeGeneration,
@@ -40,6 +42,9 @@ export interface CanonicalRuntimeDependencies {
40
42
  runMcpProbe(endpoint: string, authToken: string): Promise<void>;
41
43
  collectRuntimeSourceIdentity: typeof collectRuntimeSourceIdentity;
42
44
  rotateRuntimeGeneration: typeof rotateRuntimeGeneration;
45
+ stopLightweightProcesses(controllerHome: string): Promise<number>;
46
+ stopContextReadHelpers(): Promise<void>;
47
+ computeToolSurfaceFingerprint: typeof runtimeGatewayToolSurfaceFingerprint;
43
48
  }
44
49
 
45
50
  async function defaultMcpProbe(endpoint: string, authToken: string): Promise<void> {
@@ -77,6 +82,9 @@ const DEFAULT_DEPENDENCIES: CanonicalRuntimeDependencies = {
77
82
  runMcpProbe: defaultMcpProbe,
78
83
  collectRuntimeSourceIdentity,
79
84
  rotateRuntimeGeneration,
85
+ stopLightweightProcesses: cancelAllLightweightProcesses,
86
+ stopContextReadHelpers: closeCodeGraphReadProviderSessions,
87
+ computeToolSurfaceFingerprint: runtimeGatewayToolSurfaceFingerprint,
80
88
  };
81
89
 
82
90
  export class CanonicalForgeRuntime {
@@ -143,6 +151,17 @@ export class CanonicalForgeRuntime {
143
151
  }
144
152
  }
145
153
 
154
+ private refreshToolSurfaceFingerprint(runtimeSourceRoot: string): void {
155
+ const next = this.dependencies.computeToolSurfaceFingerprint({
156
+ controllerHome: this.config.controllerHome,
157
+ runtimeInstanceId: this.runtimeInstanceId,
158
+ runtimeSourceRoot,
159
+ });
160
+ if (next === this.toolSurfaceFingerprint) return;
161
+ this.toolSurfaceFingerprint = next;
162
+ this.publishStatus();
163
+ }
164
+
146
165
  endpoint(): string | undefined {
147
166
  return this.transport?.endpoint;
148
167
  }
@@ -218,11 +237,7 @@ export class CanonicalForgeRuntime {
218
237
  });
219
238
 
220
239
  stage = 'transport';
221
- this.toolSurfaceFingerprint = runtimeGatewayToolSurfaceFingerprint({
222
- controllerHome: this.config.controllerHome,
223
- runtimeInstanceId: this.runtimeInstanceId,
224
- runtimeSourceRoot,
225
- });
240
+ this.refreshToolSurfaceFingerprint(runtimeSourceRoot);
226
241
  this.transport = await this.dependencies.startTransport({
227
242
  host: this.config.host,
228
243
  port: this.config.port,
@@ -235,6 +250,7 @@ export class CanonicalForgeRuntime {
235
250
  sessionId,
236
251
  controllerType,
237
252
  }),
253
+ onToolSurfaceObservation: () => this.refreshToolSurfaceFingerprint(runtimeSourceRoot),
238
254
  onFatal: (error) => this.failCore('MCP_TRANSPORT_FAILED', error.message),
239
255
  });
240
256
  this.publishStatus();
@@ -298,6 +314,8 @@ export class CanonicalForgeRuntime {
298
314
  // Stop accepting new MCP work before quiescing Scheduler activity, then
299
315
  // release the Controller Home claim only after all in-process services stop.
300
316
  await this.transport?.close().catch(() => undefined);
317
+ await this.dependencies.stopLightweightProcesses(this.config.controllerHome).catch(() => undefined);
318
+ await this.dependencies.stopContextReadHelpers().catch(() => undefined);
301
319
  await this.localBridge?.close().catch(() => undefined);
302
320
  await this.scheduler?.stop().catch(() => undefined);
303
321
  const ownerPid = this.ownership?.record.pid;
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
- import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'fs';
2
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'fs';
3
3
  import { homedir } from 'os';
4
4
  import { dirname, isAbsolute, join, relative, resolve } from 'path';
5
5
  import { resolveControllerHome } from '../../cli/repositories/controller-home';
@@ -7,6 +7,7 @@ import {
7
7
  bootstrapLaunchAgentWithRetryV2,
8
8
  bootoutLaunchAgentWithRetryV2,
9
9
  installLaunchAgent,
10
+ retireConflictingForgeLaunchAgents,
10
11
  } from '../../cli/controller/launch-agents';
11
12
 
12
13
  export interface ForgeRuntimeServiceConfig {
@@ -209,52 +210,118 @@ export function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLa
209
210
  };
210
211
  }
211
212
 
212
- function atomicSymlink(path: string, target: string): void {
213
+ function pathEntryExists(path: string): boolean {
214
+ try {
215
+ lstatSync(path);
216
+ return true;
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+
222
+ function stableRuntimeEntrypointMatches(path: string, target: string): boolean {
223
+ try {
224
+ const current = lstatSync(path);
225
+ const desired = statSync(target);
226
+ if (!current.isFile() || current.isSymbolicLink() || !desired.isFile()) return false;
227
+ if (current.size !== desired.size || (current.mode & 0o777) !== (desired.mode & 0o777)) return false;
228
+ return readFileSync(path).equals(readFileSync(target));
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Keep the Runtime process at one physical path on macOS. TCC resolves symlinks
236
+ * before assigning the responsible process, so a stable symlink to an immutable
237
+ * release still creates a new permission principal for every release directory.
238
+ * The executable is Developer-ID signed with a stable designated requirement;
239
+ * atomically mirroring those exact signed bytes to this fixed service path keeps
240
+ * TCC identity stable while release assets/rollback remain bound by manifest/env.
241
+ */
242
+ function atomicMirrorRuntimeEntrypoint(path: string, target: string): void {
213
243
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
214
244
  const temporary = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
215
- rmSync(temporary, { force: true });
216
- symlinkSync(target, temporary);
217
- renameSync(temporary, path);
245
+ rmSync(temporary, { force: true, recursive: true });
246
+ try {
247
+ copyFileSync(target, temporary);
248
+ chmodSync(temporary, statSync(target).mode & 0o777);
249
+ try {
250
+ if (lstatSync(path).isDirectory()) rmSync(path, { force: true, recursive: true });
251
+ } catch {}
252
+ renameSync(temporary, path);
253
+ } finally {
254
+ rmSync(temporary, { force: true, recursive: true });
255
+ }
218
256
  }
219
257
 
220
258
  export function syncForgeRuntimeActiveEntrypoint(controllerHome: string): { path: string; target?: string; changed: boolean } {
221
259
  const paths = forgeRuntimeServicePaths(controllerHome);
222
260
  const target = activeRuntimeEntrypoint(controllerHome);
223
- if (!target) return { path: paths.activeEntrypointPath, changed: false };
224
- let current: string | undefined;
225
- try {
226
- if (lstatSync(paths.activeEntrypointPath).isSymbolicLink()) current = resolve(dirname(paths.activeEntrypointPath), readlinkSync(paths.activeEntrypointPath));
227
- } catch {}
228
- if (current === target) return { path: paths.activeEntrypointPath, target, changed: false };
229
- rmSync(paths.activeEntrypointPath, { force: true, recursive: true });
230
- atomicSymlink(paths.activeEntrypointPath, target);
261
+ if (!target) {
262
+ const existed = pathEntryExists(paths.activeEntrypointPath);
263
+ if (existed) rmSync(paths.activeEntrypointPath, { force: true, recursive: true });
264
+ return { path: paths.activeEntrypointPath, changed: existed };
265
+ }
266
+ if (stableRuntimeEntrypointMatches(paths.activeEntrypointPath, target)) {
267
+ return { path: paths.activeEntrypointPath, target, changed: false };
268
+ }
269
+ atomicMirrorRuntimeEntrypoint(paths.activeEntrypointPath, target);
270
+ if (!stableRuntimeEntrypointMatches(paths.activeEntrypointPath, target)) {
271
+ throw new Error('FORGE_RUNTIME_STABLE_ENTRYPOINT_MIRROR_FAILED');
272
+ }
231
273
  return { path: paths.activeEntrypointPath, target, changed: true };
232
274
  }
233
275
 
234
- export function ensureForgeRuntimeLaunchAgentContract(
276
+ function desiredForgeRuntimeLaunchAgentContract(
235
277
  input: { controllerHome: string; bootstrapNodeExecutable?: string; bootstrapRunnerPath?: string },
236
- ): { paths: ForgeRuntimeServicePaths; mode: 'release' | 'bootstrap'; changed: boolean } {
278
+ ): { paths: ForgeRuntimeServicePaths; mode: 'release' | 'bootstrap'; plist: string } {
237
279
  const paths = forgeRuntimeServicePaths(input.controllerHome);
238
280
  const active = activeRuntimeEntrypoint(input.controllerHome);
239
281
  const useRelease = Boolean(active);
240
282
  const releaseLaunch = useRelease ? activeRuntimeLaunchSpec(input.controllerHome) : undefined;
241
- if (useRelease) syncForgeRuntimeActiveEntrypoint(input.controllerHome);
242
283
  if (!useRelease && (!input.bootstrapNodeExecutable || !input.bootstrapRunnerPath)) {
243
284
  throw new Error('FORGE_RUNTIME_BOOTSTRAP_RUNNER_REQUIRED');
244
285
  }
245
286
  if (useRelease && !releaseLaunch) throw new Error('FORGE_RUNTIME_RELEASE_LAUNCH_SPEC_UNAVAILABLE');
246
- const plist = renderForgeRuntimeLaunchAgent({
287
+ return {
247
288
  paths,
248
- ...(useRelease
249
- ? { activeEntrypointPath: paths.activeEntrypointPath, runtimeArgs: releaseLaunch!.args, environment: releaseLaunch!.environment }
250
- : { nodeExecutable: input.bootstrapNodeExecutable!, runnerPath: input.bootstrapRunnerPath! }),
251
- });
252
- const existingSource = existsSync(paths.sourcePlistPath) ? readFileSync(paths.sourcePlistPath, 'utf8') : undefined;
253
- const existingInstalled = existsSync(paths.installedPlistPath) ? readFileSync(paths.installedPlistPath, 'utf8') : undefined;
254
- const changed = existingSource !== plist || existingInstalled !== plist;
255
- if (existingSource !== plist) atomicWrite(paths.sourcePlistPath, plist);
256
- if (existingInstalled !== plist) atomicWrite(paths.installedPlistPath, plist, 0o644);
257
- return { paths, mode: useRelease ? 'release' : 'bootstrap', changed };
289
+ mode: useRelease ? 'release' : 'bootstrap',
290
+ plist: renderForgeRuntimeLaunchAgent({
291
+ paths,
292
+ ...(useRelease
293
+ ? { activeEntrypointPath: paths.activeEntrypointPath, runtimeArgs: releaseLaunch!.args, environment: releaseLaunch!.environment }
294
+ : { nodeExecutable: input.bootstrapNodeExecutable!, runnerPath: input.bootstrapRunnerPath! }),
295
+ }),
296
+ };
297
+ }
298
+
299
+ export function inspectForgeRuntimeLaunchAgentContract(
300
+ input: { controllerHome: string; bootstrapNodeExecutable?: string; bootstrapRunnerPath?: string; inspectUserLaunchAgent?: boolean },
301
+ ): { paths: ForgeRuntimeServicePaths; mode: 'release' | 'bootstrap'; sourceMatches: boolean; installedMatches: boolean; matches: boolean } {
302
+ const desired = desiredForgeRuntimeLaunchAgentContract(input);
303
+ const existingSource = existsSync(desired.paths.sourcePlistPath) ? readFileSync(desired.paths.sourcePlistPath, 'utf8') : undefined;
304
+ const existingInstalled = input.inspectUserLaunchAgent && existsSync(desired.paths.installedPlistPath)
305
+ ? readFileSync(desired.paths.installedPlistPath, 'utf8')
306
+ : undefined;
307
+ const sourceMatches = existingSource === desired.plist;
308
+ const installedMatches = input.inspectUserLaunchAgent !== true || existingInstalled === desired.plist;
309
+ return { paths: desired.paths, mode: desired.mode, sourceMatches, installedMatches, matches: sourceMatches && installedMatches };
310
+ }
311
+
312
+ export function ensureForgeRuntimeLaunchAgentContract(
313
+ input: { controllerHome: string; bootstrapNodeExecutable?: string; bootstrapRunnerPath?: string; installUserLaunchAgent?: boolean },
314
+ ): { paths: ForgeRuntimeServicePaths; mode: 'release' | 'bootstrap'; changed: boolean } {
315
+ const desired = desiredForgeRuntimeLaunchAgentContract(input);
316
+ if (desired.mode === 'release') syncForgeRuntimeActiveEntrypoint(input.controllerHome);
317
+ const existingSource = existsSync(desired.paths.sourcePlistPath) ? readFileSync(desired.paths.sourcePlistPath, 'utf8') : undefined;
318
+ const existingInstalled = input.installUserLaunchAgent && existsSync(desired.paths.installedPlistPath)
319
+ ? readFileSync(desired.paths.installedPlistPath, 'utf8')
320
+ : undefined;
321
+ const changed = existingSource !== desired.plist || (input.installUserLaunchAgent === true && existingInstalled !== desired.plist);
322
+ if (existingSource !== desired.plist) atomicWrite(desired.paths.sourcePlistPath, desired.plist);
323
+ if (input.installUserLaunchAgent === true && existingInstalled !== desired.plist) atomicWrite(desired.paths.installedPlistPath, desired.plist, 0o644);
324
+ return { paths: desired.paths, mode: desired.mode, changed };
258
325
  }
259
326
 
260
327
  export function renderForgeRuntimeLaunchAgent(input: { paths: ForgeRuntimeServicePaths; activeEntrypointPath?: string; runtimeArgs?: string[]; environment?: Record<string, string>; nodeExecutable?: string; runnerPath?: string }): string {
@@ -286,6 +353,12 @@ export async function installForgeRuntimeService(input: { config: ForgeRuntimeSe
286
353
  bootstrapRunnerPath: input.runnerPath,
287
354
  });
288
355
  await retireLegacyBrowserAutomationLaunchAgent(config.controllerHome);
356
+ await retireConflictingForgeLaunchAgents({
357
+ desiredLabel: paths.label,
358
+ labelPrefix: 'com.moretea.forge.runtime.',
359
+ port: config.port,
360
+ requiredArguments: ['--controller-home'],
361
+ });
289
362
  installLaunchAgent(paths.sourcePlistPath, paths.label);
290
363
  const result = await bootstrapLaunchAgentWithRetryV2({ label: paths.label, plistPath: paths.installedPlistPath });
291
364
  if (!result.ok) throw new Error(`FORGE_RUNTIME_SERVICE_BOOTSTRAP_FAILED: ${result.diagnostics.join('; ')}`);