@moretea-labs/forge 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +46 -0
  3. package/CLAUDE.md +2 -0
  4. package/DESIGN.md +7 -2
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +2 -2
  7. package/SKILL.md +2 -2
  8. package/THIRD_PARTY_NOTICES.md +1 -1
  9. package/agents/openai.yaml +1 -1
  10. package/assets/hooks/hook-input.sh +11 -8
  11. package/assets/hooks/lib/session-state.sh +10 -7
  12. package/assets/hooks/lib/workflow-state.sh +13 -177
  13. package/assets/hooks/post-tool-observer.sh +8 -2
  14. package/assets/hooks/pre-edit-guard.sh +8 -39
  15. package/assets/hooks/prompt-guard.sh +7 -83
  16. package/assets/hooks/session-start-context.sh +56 -1
  17. package/assets/partials/05-workflow.partial.md +1 -1
  18. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  19. package/assets/plugin-registry.v1.json +54 -3
  20. package/assets/reference-configs/document-generation.md +1 -1
  21. package/assets/reference-configs/harness-overview.md +1 -1
  22. package/assets/reference-configs/hook-operations.md +1 -1
  23. package/assets/reference-configs/sprint-contracts.md +3 -3
  24. package/assets/skill-version.json +2 -2
  25. package/assets/templates/contract.template.md +2 -7
  26. package/assets/templates/helpers/capture-plan.sh +2 -2
  27. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  28. package/assets/templates/helpers/contract-worktree.sh +1 -21
  29. package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/new-plan.sh +1 -1
  32. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  33. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  34. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/plan.template.md +1 -1
  38. package/assets/templates/review.template.md +9 -34
  39. package/assets/workflow-contract.v1.json +10 -19
  40. package/docs/README.md +10 -10
  41. package/docs/architecture/CURRENT.md +170 -0
  42. package/docs/architecture/EVOLUTION.md +42 -0
  43. package/docs/architecture/current/README.md +4 -156
  44. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  45. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  46. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  47. package/docs/architecture/history/README.md +7 -0
  48. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  49. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  50. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  51. package/docs/architecture/history.md +3 -34
  52. package/docs/architecture/index.md +15 -98
  53. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  54. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  55. package/docs/architecture/versions/1.6.md +38 -0
  56. package/docs/architecture/versions/1.7.md +41 -0
  57. package/docs/forge-chatgpt-browser-engine.md +2 -2
  58. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  59. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  60. package/docs/forge-github-issue-launcher.md +1 -1
  61. package/docs/forge-runtime-self-healing-loop.md +1 -1
  62. package/docs/operations/controller-browser-plugin.md +38 -22
  63. package/docs/operations/controller-performance-and-502.md +124 -0
  64. package/docs/operations/controller-reliability-runbook.md +1 -1
  65. package/docs/operations/features.md +3 -3
  66. package/docs/operations/github-repository.md +1 -1
  67. package/docs/operations/homebrew.md +2 -2
  68. package/docs/operations/mcp-tool-exposure.md +5 -3
  69. package/docs/tutorials/01-install-and-start.md +1 -1
  70. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Installation.md +1 -1
  74. package/docs/wiki/Integrations.md +2 -2
  75. package/docs/wiki/Quick-Start.md +1 -1
  76. package/docs/wiki/Work-Lifecycle.md +1 -1
  77. package/package.json +29 -10
  78. package/scripts/activate-source-baseline.ts +17 -13
  79. package/scripts/benchmark-background-check-overlap.ts +228 -0
  80. package/scripts/benchmark-mcp-host.sh +117 -0
  81. package/scripts/benchmark-mcp-transport.ts +170 -0
  82. package/scripts/benchmark-quality-harness.ts +146 -0
  83. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  84. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  85. package/scripts/benchmark-typescript-navigation.ts +186 -0
  86. package/scripts/benchmark-work-efficiency.ts +334 -0
  87. package/scripts/capture-plan.sh +3 -3
  88. package/scripts/check-agent-tooling.sh +8 -35
  89. package/scripts/check-architecture-sync.sh +17 -86
  90. package/scripts/check-mcp-compatibility.ts +14 -0
  91. package/scripts/check-public-docs.sh +14 -1
  92. package/scripts/check-runtime-architecture.mjs +107 -64
  93. package/scripts/check-stable-baseline.ts +92 -0
  94. package/scripts/check-task-workflow.sh +4 -4
  95. package/scripts/contract-worktree.sh +1 -21
  96. package/scripts/ensure-task-workflow.sh +22 -55
  97. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  98. package/scripts/install-standalone-recovery.ts +24 -0
  99. package/scripts/lib/project-init-lib.sh +15 -44
  100. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  101. package/scripts/migrate-project-template.sh +23 -1179
  102. package/scripts/new-plan.sh +1 -1
  103. package/scripts/plan-to-todo.sh +11 -36
  104. package/scripts/run-governed-gate.ts +2 -1
  105. package/scripts/ship-worktrees.sh +2 -29
  106. package/scripts/smoke-ios-physical-device.ts +4 -2
  107. package/scripts/smoke-runtime-recovery.ts +1 -35
  108. package/scripts/smoke-schedule-engine.ts +1 -20
  109. package/scripts/sprint-backlog.sh +1 -1
  110. package/scripts/verify-contract.sh +1 -111
  111. package/scripts/verify-forge-runtime.sh +0 -1
  112. package/scripts/verify-sprint.sh +3 -180
  113. package/scripts/workflow-contract.ts +1 -5
  114. package/src/cli/commands/chatgpt.ts +31 -2
  115. package/src/cli/commands/init-hook.ts +3 -3
  116. package/src/cli/commands/mcp.ts +9 -5
  117. package/src/cli/commands/plugin.ts +37 -10
  118. package/src/cli/commands/recovery.ts +25 -3
  119. package/src/cli/commands/runtime.ts +55 -3
  120. package/src/cli/commands/security.ts +12 -1
  121. package/src/cli/commands/setup-profile.ts +31 -21
  122. package/src/cli/controller/check-runner.ts +106 -3
  123. package/src/cli/controller/composite-result.ts +1 -1
  124. package/src/cli/controller/context/focus.ts +51 -0
  125. package/src/cli/controller/context/instruction-context.ts +114 -0
  126. package/src/cli/controller/context/known-paths.ts +76 -0
  127. package/src/cli/controller/context/query-planning.ts +110 -0
  128. package/src/cli/controller/context/rule-resolution.ts +532 -0
  129. package/src/cli/controller/context/source-materializer.ts +278 -0
  130. package/src/cli/controller/context/structural.ts +16 -0
  131. package/src/cli/controller/context/types.ts +286 -0
  132. package/src/cli/controller/context-pack.ts +898 -430
  133. package/src/cli/controller/launch-agents.ts +77 -2
  134. package/src/cli/controller/operational-plan.ts +2 -2
  135. package/src/cli/controller/work-mode.ts +6 -4
  136. package/src/cli/editing/edit-session.ts +60 -7
  137. package/src/cli/github/github.ts +133 -0
  138. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  139. package/src/cli/hook/prompt-intents.ts +1 -1
  140. package/src/cli/installer/managed-entries.ts +15 -1
  141. package/src/cli/local-bridge/console-automations.ts +225 -67
  142. package/src/cli/local-bridge/console-view-models.ts +3 -35
  143. package/src/cli/local-bridge/facade-api.ts +2 -217
  144. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  145. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
  146. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  147. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
  148. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  149. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  150. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
  151. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  152. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  153. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
  154. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  155. package/src/cli/local-bridge/frontend/src/types.ts +8 -7
  156. package/src/cli/local-bridge/job-store.ts +1 -490
  157. package/src/cli/local-bridge/server.ts +29 -532
  158. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  159. package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
  160. package/src/cli/mcp/auth.ts +5 -5
  161. package/src/cli/mcp/instructions.ts +3 -2
  162. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  163. package/src/cli/mcp/multi-repository.ts +16 -5
  164. package/src/cli/mcp/oauth.ts +175 -26
  165. package/src/cli/mcp/paths.ts +8 -2
  166. package/src/cli/mcp/repository-tools.ts +356 -101
  167. package/src/cli/mcp/server.ts +625 -54
  168. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  169. package/src/cli/mcp/toolset-selection.ts +1 -1
  170. package/src/cli/mcp/toolset.ts +42 -5
  171. package/src/cli/mcp/transports/http.ts +125 -15
  172. package/src/cli/mcp/transports/session-registry.ts +67 -5
  173. package/src/cli/mcp/types.ts +3 -3
  174. package/src/cli/repositories/command-classifier.ts +209 -14
  175. package/src/cli/repositories/command-executor.ts +55 -521
  176. package/src/cli/repositories/command-process.ts +108 -0
  177. package/src/cli/repositories/command-scope.ts +99 -41
  178. package/src/cli/repositories/controller-home.ts +7 -8
  179. package/src/cli/repositories/git-command-executor.ts +2 -0
  180. package/src/cli/repositories/locks.ts +53 -16
  181. package/src/cli/repositories/repository-snapshot.ts +121 -0
  182. package/src/cli/repositories/safe-patch.ts +6 -4
  183. package/src/cli/repositories/selected-path-actions.ts +17 -9
  184. package/src/cli/repositories/structured-git.ts +110 -10
  185. package/src/cli/repository/inspector.ts +565 -62
  186. package/src/cli/repository/session-cache.ts +58 -17
  187. package/src/cli/v81-entry.ts +1 -4
  188. package/src/effects/fs-transaction.ts +85 -17
  189. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  190. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  191. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  192. package/src/runtime/context/lsp-client.ts +283 -0
  193. package/src/runtime/context/semantic-navigation.ts +275 -0
  194. package/src/runtime/context/swift-navigation.ts +565 -0
  195. package/src/runtime/context/typescript-navigation.ts +226 -0
  196. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  197. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
  198. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  199. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  200. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  201. package/src/runtime/control-plane/execution/validation.ts +15 -2
  202. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  203. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  204. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  205. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  206. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  207. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
  208. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
  209. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
  210. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  211. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  212. package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
  213. package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
  214. package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
  215. package/src/runtime/control-plane/facade/index.ts +1 -0
  216. package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
  217. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  218. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  219. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  220. package/src/runtime/control-plane/facade/types.ts +55 -11
  221. package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
  222. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  223. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  224. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  225. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  226. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  227. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  228. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  229. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  230. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  231. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  232. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  233. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  234. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  235. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  236. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  237. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  238. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  239. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  240. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  241. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
  242. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  243. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  244. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  245. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  246. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  247. package/src/runtime/control-plane/release-retention.ts +62 -2
  248. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  249. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  250. package/src/runtime/diagnostics/mcp-timing.ts +19 -0
  251. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  252. package/src/runtime/diagnostics/performance.ts +24 -1
  253. package/src/runtime/evidence/event-ledger.ts +1 -1
  254. package/src/runtime/execution/jobs/store.ts +58 -4
  255. package/src/runtime/execution/jobs/types.ts +1 -1
  256. package/src/runtime/execution/managed-workspace.ts +203 -1
  257. package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
  258. package/src/runtime/execution/process-runtime/check-result.ts +63 -2
  259. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  260. package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
  261. package/src/runtime/execution/process-runtime/gc.ts +98 -12
  262. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  263. package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
  264. package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
  265. package/src/runtime/execution/process-runtime/runtime.ts +59 -4
  266. package/src/runtime/execution/process-runtime/store.ts +7 -0
  267. package/src/runtime/execution/process-runtime/types.ts +2 -0
  268. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  269. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  270. package/src/runtime/execution/workers/executor.ts +0 -48
  271. package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
  272. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  273. package/src/runtime/gateway/mcp/router.ts +185 -83
  274. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  275. package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
  276. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  277. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  278. package/src/runtime/plugins/browser-adapter.ts +2925 -318
  279. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  280. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  281. package/src/runtime/plugins/browser-handoff.ts +92 -5
  282. package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
  283. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  284. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  285. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  286. package/src/runtime/plugins/browser-runtime.ts +183 -0
  287. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  288. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  289. package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
  290. package/src/runtime/plugins/external-adapter.ts +324 -3
  291. package/src/runtime/plugins/external-registration.ts +84 -5
  292. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  293. package/src/runtime/plugins/first-party-registry.ts +4 -2
  294. package/src/runtime/plugins/github-adapter.ts +125 -0
  295. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  296. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  297. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  298. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  299. package/src/runtime/plugins/ios-adapter.ts +106 -16
  300. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  301. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  302. package/src/runtime/plugins/lightweight-action.ts +153 -0
  303. package/src/runtime/plugins/local-system-adapter.ts +125 -8
  304. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  305. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  306. package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
  307. package/src/runtime/plugins/resend-adapter.ts +1 -1
  308. package/src/runtime/plugins/store.ts +366 -24
  309. package/src/runtime/plugins/types.ts +39 -2
  310. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  311. package/src/runtime/projections/git-status-sampler.ts +16 -16
  312. package/src/runtime/projections/materialized-view.ts +28 -10
  313. package/src/runtime/recovery/actions.ts +0 -10
  314. package/src/runtime/recovery/classifier.ts +0 -9
  315. package/src/runtime/recovery/maintenance-executor.ts +415 -18
  316. package/src/runtime/recovery/probe.ts +0 -1
  317. package/src/runtime/recovery/types.ts +0 -1
  318. package/src/runtime/root/mcp-transport.ts +180 -10
  319. package/src/runtime/root/package-connector-service.ts +186 -8
  320. package/src/runtime/root/package-runtime-release.ts +143 -17
  321. package/src/runtime/root/package-runtime-service.ts +430 -22
  322. package/src/runtime/root/release-manifest.ts +7 -2
  323. package/src/runtime/root/release-materialize.ts +162 -19
  324. package/src/runtime/root/release-store.ts +48 -11
  325. package/src/runtime/root/runtime.ts +23 -5
  326. package/src/runtime/root/service.ts +100 -27
  327. package/src/runtime/root/types.ts +2 -0
  328. package/src/runtime/safe-tooling/index.ts +0 -1
  329. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  330. package/src/runtime/safe-tooling/types.ts +0 -30
  331. package/src/runtime/shared/trusted-executable.ts +62 -0
  332. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  333. package/src/runtime/standalone-recovery/core.ts +702 -94
  334. package/src/runtime/standalone-recovery/entry.ts +99 -23
  335. package/src/runtime/standalone-recovery/installer.ts +19 -3
  336. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  337. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  338. package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
  339. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  340. package/src/runtime/workflow/schedules/engine.ts +509 -80
  341. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  342. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  343. package/src/runtime/workflow/schedules/store.ts +131 -10
  344. package/src/runtime/workflow/schedules/types.ts +6 -3
  345. package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
  346. package/src/testing/test-governance.ts +8 -40
  347. package/assets/reference-configs/heartbeat-triage.md +0 -57
  348. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  349. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  350. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  351. package/bin/forge-desktop-helper.mjs +0 -204
  352. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  353. package/docs/architecture/current/architecture-invariants.md +0 -331
  354. package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
  355. package/docs/architecture/current/codegraph-context-provider.md +0 -226
  356. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  357. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  358. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
  359. package/docs/architecture/current/entity-model.md +0 -592
  360. package/docs/architecture/current/external-plugin-governance.md +0 -396
  361. package/docs/architecture/current/failure-recovery.md +0 -447
  362. package/docs/architecture/current/governance.md +0 -213
  363. package/docs/architecture/current/human-interaction-plane.md +0 -87
  364. package/docs/architecture/current/implementation-status.md +0 -128
  365. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  366. package/docs/architecture/current/legacy-governance.md +0 -58
  367. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  368. package/docs/architecture/current/migration-roadmap.md +0 -105
  369. package/docs/architecture/current/multi-repository-execution.md +0 -371
  370. package/docs/architecture/current/operations-runbook.md +0 -45
  371. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  372. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  373. package/docs/architecture/current/repository-access-modes.md +0 -102
  374. package/docs/architecture/current/route-policy.md +0 -15
  375. package/docs/architecture/current/route-session-concurrency.md +0 -85
  376. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  377. package/docs/architecture/current/runtime-directory-map.md +0 -49
  378. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  379. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  380. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  381. package/docs/architecture/current/system-overview.md +0 -144
  382. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  383. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  384. package/docs/architecture/current/thin-harness-v1.md +0 -311
  385. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  386. package/docs/forge-autonomous-goal-loop.md +0 -9
  387. package/docs/forge-provider-configuration.md +0 -7
  388. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
  389. package/docs/operations/controller-desktop-plugin.md +0 -112
  390. package/docs/operations/gmail-assistant-routines.md +0 -186
  391. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  392. package/docs/operations/provider-configuration.md +0 -31
  393. package/scripts/harness-trace-grade.sh +0 -150
  394. package/scripts/heartbeat-triage.sh +0 -361
  395. package/scripts/maintenance-triage.sh +0 -167
  396. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  397. package/src/cli/repositories/goal-registry.ts +0 -239
  398. package/src/runtime/assistant/action-proposals.ts +0 -279
  399. package/src/runtime/assistant/intent.ts +0 -395
  400. package/src/runtime/assistant/model-provider.ts +0 -460
  401. package/src/runtime/assistant/openapi.ts +0 -223
  402. package/src/runtime/assistant/policy.ts +0 -89
  403. package/src/runtime/assistant/readiness.ts +0 -258
  404. package/src/runtime/assistant/routine-runtime.ts +0 -477
  405. package/src/runtime/assistant/schedule-binding.ts +0 -171
  406. package/src/runtime/assistant/standing-grants.ts +0 -375
  407. package/src/runtime/assistant/store.ts +0 -243
  408. package/src/runtime/assistant/types.ts +0 -135
  409. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  410. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  411. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  412. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  413. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  414. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  415. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  416. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  417. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  418. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  419. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  420. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  421. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  422. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  423. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  424. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  425. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  426. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  427. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  428. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  429. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
  430. package/src/runtime/workflow/findings/store.ts +0 -204
  431. package/src/runtime/workflow/findings/types.ts +0 -58
  432. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  433. package/src/runtime/workflow/portfolio/store.ts +0 -185
  434. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
2
  import { createServer, type IncomingMessage, type ServerResponse } from 'http';
3
- import { basename, resolve } from 'path';
3
+ import { basename, join, resolve } from 'path';
4
4
  import { pathToFileURL } from 'url';
5
5
  import { readMcpServiceOAuthPassphrase } from '../../cli/mcp/auth';
6
6
  import { FORGE_VERSION } from '../../version';
@@ -18,6 +18,7 @@ import {
18
18
  repairPublicTunnel,
19
19
  restartPrimaryConnector,
20
20
  restartPrimaryRuntime,
21
+ restartRecoveryWatchdog,
21
22
  stageAndActivateConfiguredRuntimeRelease,
22
23
  rollbackPrevious,
23
24
  secureEqual,
@@ -27,6 +28,12 @@ import {
27
28
  type WatchdogState,
28
29
  type RecoveryConfig,
29
30
  } from './core';
31
+ import {
32
+ createRecoveryWatchdogHeartbeat,
33
+ observeRecoveryWatchdogHealth,
34
+ writeRecoveryWatchdogHeartbeat,
35
+ type RecoveryWatchdogHeartbeat,
36
+ } from './watchdog-heartbeat';
30
37
  import {
31
38
  RECOVERY_RELEASE_ROLE_CANARY_ARG,
32
39
  writeRecoveryRuntimeIdentity,
@@ -139,12 +146,24 @@ export function resetWatchdogStateForRecoveryRelease(
139
146
  // budget. Those counters are bound to the Runtime release and survive both
140
147
  // watchdog process exits and immutable Recovery release activation.
141
148
  recoveryGatewayRestartUsed: false,
149
+ primaryConnectorFailures: 0,
150
+ primaryConnectorFirstFailureAt: undefined,
151
+ primaryConnectorRestartAttempts: 0,
152
+ primaryConnectorRestartFailures: 0,
153
+ primaryConnectorRestartLastAttemptAt: undefined,
142
154
  recoveryReleaseRevision: releaseRevision,
143
155
  };
144
156
  }
145
157
 
146
158
  async function startWatchdog(config: RecoveryConfig): Promise<void> {
147
159
  const runtimeIdentity = writeRecoveryRuntimeIdentity(config.controllerHome, 'watchdog');
160
+ let heartbeat: RecoveryWatchdogHeartbeat = createRecoveryWatchdogHeartbeat(runtimeIdentity);
161
+ const persistHeartbeat = (patch: Partial<RecoveryWatchdogHeartbeat> = {}) => {
162
+ heartbeat = writeRecoveryWatchdogHeartbeat(config.controllerHome, { ...heartbeat, ...patch });
163
+ };
164
+ persistHeartbeat();
165
+ const pulse = setInterval(() => persistHeartbeat({ lastPulseAt: new Date().toISOString() }), 5_000);
166
+ pulse.unref?.();
148
167
  process.stdout.write(JSON.stringify({ status: 'ready', role: 'watchdog', runtimeIdentity }) + '\n');
149
168
  const loadedState = loadWatchdogState(config);
150
169
  let state = runtimeIdentity?.releaseRevision
@@ -152,9 +171,13 @@ async function startWatchdog(config: RecoveryConfig): Promise<void> {
152
171
  : loadedState;
153
172
  if (state !== loadedState) state = saveWatchdogState(config, state);
154
173
  for (;;) {
174
+ const tickStartedAt = new Date().toISOString();
175
+ persistHeartbeat({ lastPulseAt: tickStartedAt, lastTickStartedAt: tickStartedAt, lastError: undefined });
155
176
  try {
156
177
  const result = await watchdogTick(config, state);
157
178
  state = saveWatchdogState(config, result.state);
179
+ const tickCompletedAt = new Date().toISOString();
180
+ persistHeartbeat({ lastPulseAt: tickCompletedAt, lastTickCompletedAt: tickCompletedAt, lastError: undefined });
158
181
  process.stdout.write(JSON.stringify({
159
182
  at: new Date().toISOString(),
160
183
  action: result.decision.action,
@@ -170,10 +193,14 @@ async function startWatchdog(config: RecoveryConfig): Promise<void> {
170
193
  primaryRuntimeRecoveryDetail: result.primaryRuntimeRecovery?.detail,
171
194
  rollbackDetail: result.rollback?.detail,
172
195
  publicTunnelDetail: result.publicTunnelRepair?.detail,
196
+ primaryConnectorRestartDetail: result.primaryConnectorRestart?.detail,
173
197
  }) + '\n');
174
198
  } catch (error) {
175
199
  state = saveWatchdogState(config, { ...state, failures: state.failures + 1, firstFailureAt: state.firstFailureAt ?? Date.now() });
176
- process.stderr.write(`watchdog probe failed: ${error instanceof Error ? error.message : String(error)}\n`);
200
+ const detail = error instanceof Error ? error.message : String(error);
201
+ const failedAt = new Date().toISOString();
202
+ persistHeartbeat({ lastPulseAt: failedAt, lastTickFailedAt: failedAt, lastError: detail.slice(0, 500) });
203
+ process.stderr.write(`watchdog probe failed: ${detail}\n`);
177
204
  }
178
205
  await new Promise((resolveSleep) => setTimeout(resolveSleep, 5_000));
179
206
  }
@@ -228,7 +255,7 @@ export const RECOVERY_TOOLS = [
228
255
  { name: 'restart_primary_runtime', description: 'Restart the installed canonical Forge Runtime service and require whole-Runtime verification.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
229
256
  { name: 'restart_primary_connector', description: 'Restart the explicitly configured primary OAuth/Connector launchd service only after local Canonical Runtime verification succeeds.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
230
257
  { name: 'recover_primary_runtime', description: 'Stop the canonical Runtime, restore the attested previous whole release and SQLite backup, restart it, and require verification.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
231
- { name: 'activate_runtime_release', description: 'Activate an already staged immutable Runtime release only if the caller-observed active release/authority revision are still current. Reverse activation of current.previous is rejected; use rollback_previous/recover_primary_runtime instead.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 }, release_path: { type: 'string', minLength: 8, maxLength: 1024 }, expected_active_release_id: { type: 'string', minLength: 1, maxLength: 256 }, expected_authority_revision: { type: 'integer', minimum: 1 } }, required: ['request_id', 'release_path', 'expected_active_release_id', 'expected_authority_revision'], additionalProperties: false } },
258
+ { name: 'activate_runtime_release', description: 'Activate an already staged immutable Runtime release only if the caller-observed active release/authority revision are still current. Reverse activation of current.previous is rejected; use rollback_previous/recover_primary_runtime instead.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 }, release_path: { type: 'string', minLength: 8, maxLength: 1024, description: 'Absolute path to the staged immutable Runtime release directory.' }, expected_active_release_id: { type: 'string', minLength: 1, maxLength: 256 }, expected_authority_revision: { type: 'integer', minimum: 1 } }, required: ['request_id', 'release_path', 'expected_active_release_id', 'expected_authority_revision'], additionalProperties: false } },
232
259
  { name: 'stage_and_activate_runtime_release', description: 'Build one immutable Runtime release from the fixed Recovery-configured source root, then activate it transactionally with rollback protection. No arbitrary source path is accepted.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
233
260
  { name: 'restart_public_tunnel', description: 'Restart the explicitly configured public tunnel only after local runtime verification succeeds and the external endpoint is unavailable.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
234
261
  { name: 'reconnect_primary_connector', description: 'Check canonical Runtime Gateway and primary MCP reconnection readiness without publishing a release.', inputSchema: { type: 'object', additionalProperties: false } },
@@ -277,7 +304,8 @@ function requestUrl(request: IncomingMessage): URL {
277
304
  return new URL(request.url ?? '/', `${proto}://${host}`);
278
305
  }
279
306
 
280
- function publicOrigin(request: IncomingMessage): string {
307
+ function publicOrigin(request: IncomingMessage, config?: Pick<RecoveryConfig, 'recoveryPublicUrl'>): string {
308
+ if (config?.recoveryPublicUrl) return new URL(config.recoveryPublicUrl).origin;
281
309
  const url = requestUrl(request);
282
310
  return `${url.protocol}//${url.host}`;
283
311
  }
@@ -289,12 +317,12 @@ function resourcePathFromRequest(_request: IncomingMessage): '/recovery/mcp' {
289
317
  return '/recovery/mcp';
290
318
  }
291
319
 
292
- function recoveryResource(request: IncomingMessage): string {
293
- return `${publicOrigin(request)}${resourcePathFromRequest(request)}`;
320
+ function recoveryResource(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): string {
321
+ return `${publicOrigin(request, config)}${resourcePathFromRequest(request)}`;
294
322
  }
295
323
 
296
- function recoveryAuthorizationServerMetadata(request: IncomingMessage): Record<string, unknown> {
297
- const origin = publicOrigin(request);
324
+ function recoveryAuthorizationServerMetadata(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): Record<string, unknown> {
325
+ const origin = publicOrigin(request, config);
298
326
  return {
299
327
  issuer: origin,
300
328
  authorization_endpoint: `${origin}/recovery/oauth/authorize`,
@@ -308,19 +336,37 @@ function recoveryAuthorizationServerMetadata(request: IncomingMessage): Record<s
308
336
  };
309
337
  }
310
338
 
311
- function recoveryProtectedResourceMetadata(request: IncomingMessage): Record<string, unknown> {
339
+ function recoveryProtectedResourceMetadata(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): Record<string, unknown> {
340
+ const origin = publicOrigin(request, config);
312
341
  return {
313
- resource: recoveryResource(request),
314
- authorization_servers: [publicOrigin(request)],
342
+ resource: recoveryResource(request, config),
343
+ authorization_servers: [origin],
315
344
  scopes_supported: [RECOVERY_OAUTH_SCOPE],
316
345
  bearer_methods_supported: ['header'],
317
- resource_documentation: `${publicOrigin(request)}/recovery/health`,
346
+ resource_documentation: `${origin}/recovery/health`,
318
347
  };
319
348
  }
320
349
 
321
- function recoveryWwwAuthenticate(request: IncomingMessage): string {
322
- const metadata = `${publicOrigin(request)}/.well-known/oauth-protected-resource${resourcePathFromRequest(request)}`;
323
- return `Bearer realm="forge-recovery", resource_metadata="${metadata}", scope="${RECOVERY_OAUTH_SCOPE}"`;
350
+ export function recoveryWwwAuthenticate(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): string {
351
+ const metadata = `${publicOrigin(request, config)}/.well-known/oauth-protected-resource${resourcePathFromRequest(request)}`;
352
+ return `Bearer error="invalid_token", error_description="Missing Authorization header", resource_metadata="${metadata}"`;
353
+ }
354
+
355
+ export function recoveryUnauthorizedBody(): { error: string; message: string } {
356
+ return { error: 'invalid_token', message: 'Missing Authorization header' };
357
+ }
358
+
359
+ export type RecoveryMcpRequestClassification = 'not_mcp' | 'auth_required' | 'method_not_supported' | 'mcp';
360
+
361
+ export function classifyRecoveryMcpRequest(
362
+ request: Pick<IncomingMessage, 'method' | 'url' | 'headers'>,
363
+ expectedToken: string | undefined,
364
+ ): RecoveryMcpRequestClassification {
365
+ if (!matchesAnyPath(request.url, ['/mcp', '/recovery/mcp'])) return 'not_mcp';
366
+ const supplied = request.headers.authorization?.replace(/^Bearer\s+/i, '').trim();
367
+ if (!expectedToken || !supplied || !secureEqual(supplied, expectedToken)) return 'auth_required';
368
+ if (request.method !== 'POST') return 'method_not_supported';
369
+ return 'mcp';
324
370
  }
325
371
 
326
372
  function parseUrlEncoded(input: string): URLSearchParams {
@@ -473,7 +519,9 @@ export async function dispatchRecoveryTool(config: RecoveryConfig, name: string,
473
519
  if (typeof args.release_path !== 'string' || !args.release_path.trim()) throw new Error('RECOVERY_RELEASE_PATH_REQUIRED');
474
520
  if (typeof args.expected_active_release_id !== 'string' || !args.expected_active_release_id.trim()) throw new Error('RECOVERY_EXPECTED_ACTIVE_RELEASE_REQUIRED');
475
521
  if (!Number.isInteger(args.expected_authority_revision) || Number(args.expected_authority_revision) < 1) throw new Error('RECOVERY_EXPECTED_AUTHORITY_REVISION_REQUIRED');
476
- return activateRuntimeRelease(config, args.release_path.trim(), {}, {
522
+ const releasePath = args.release_path.trim();
523
+ const manifestPath = basename(releasePath) === 'manifest.json' ? releasePath : join(releasePath, 'manifest.json');
524
+ return activateRuntimeRelease(config, manifestPath, {}, {
477
525
  requestId: `recovery-gateway:${args.request_id}`,
478
526
  expectedActiveReleaseId: args.expected_active_release_id.trim(),
479
527
  expectedAuthorityRevision: Number(args.expected_authority_revision),
@@ -498,6 +546,7 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
498
546
  throw new Error('RECOVERY_GATEWAY_CONFIG_INVALID');
499
547
  }
500
548
  let runtimeIdentity: RecoveryRuntimeIdentity | undefined;
549
+ let watchdogRestartInFlight = false;
501
550
  const recentMutations = new Map<string, number[]>();
502
551
  const oauthCodes = new Map<string, PendingOAuthCode>();
503
552
  const oauthClients = new Map<string, OAuthClient>();
@@ -509,9 +558,18 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
509
558
  return;
510
559
  }
511
560
  if (request.method === 'GET' && matchesAnyPath(request.url, ['/health', '/recovery/health'])) {
561
+ const watchdog = observeRecoveryWatchdogHealth(config.controllerHome);
512
562
  json(response, 200, {
513
- status: 'ok',
563
+ status: watchdog.ok ? 'ok' : 'degraded',
514
564
  service: 'forge-standalone-recovery',
565
+ watchdog: {
566
+ ok: watchdog.ok,
567
+ detail: watchdog.detail,
568
+ pulseAgeMs: watchdog.pulseAgeMs,
569
+ tickAgeMs: watchdog.tickAgeMs,
570
+ releaseRevision: watchdog.runtimeIdentity?.releaseRevision,
571
+ pid: watchdog.runtimeIdentity?.pid,
572
+ },
515
573
  version: FORGE_VERSION,
516
574
  ...(runtimeIdentity ? {
517
575
  releasePath: runtimeIdentity.releasePath,
@@ -522,8 +580,8 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
522
580
  });
523
581
  return;
524
582
  }
525
- if (request.method === 'GET' && isProtectedResourceMetadataPath(request)) { json(response, 200, recoveryProtectedResourceMetadata(request)); return; }
526
- if (request.method === 'GET' && isOAuthMetadataPath(request)) { json(response, 200, recoveryAuthorizationServerMetadata(request)); return; }
583
+ if (request.method === 'GET' && isProtectedResourceMetadataPath(request)) { json(response, 200, recoveryProtectedResourceMetadata(request, config)); return; }
584
+ if (request.method === 'GET' && isOAuthMetadataPath(request)) { json(response, 200, recoveryAuthorizationServerMetadata(request, config)); return; }
527
585
  if ((request.method === 'GET' || request.method === 'POST') && isAuthorizePath(request)) {
528
586
  const params = request.method === 'GET'
529
587
  ? requestUrl(request).searchParams
@@ -640,10 +698,9 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
640
698
  });
641
699
  return;
642
700
  }
643
- if (request.method !== 'POST' || !matchesAnyPath(request.url, ['/mcp', '/recovery/mcp'])) { json(response, 404, { error: 'NOT_FOUND' }); return; }
644
- const expected = gatewayToken(config);
645
- const supplied = request.headers.authorization?.replace(/^Bearer\s+/i, '').trim();
646
- if (!expected || !supplied || !secureEqual(supplied, expected)) { response.setHeader('www-authenticate', recoveryWwwAuthenticate(request)); json(response, 401, { error: 'RECOVERY_AUTH_REQUIRED' }); return; }
701
+ const mcpRequest = classifyRecoveryMcpRequest(request, gatewayToken(config));
702
+ if (mcpRequest === 'not_mcp' || mcpRequest === 'method_not_supported') { json(response, 404, { error: 'NOT_FOUND' }); return; }
703
+ if (mcpRequest === 'auth_required') { response.setHeader('www-authenticate', recoveryWwwAuthenticate(request, config)); json(response, 401, recoveryUnauthorizedBody()); return; }
647
704
  if (!/^application\/json(?:\s*;|$)/i.test(String(request.headers['content-type'] ?? ''))) { json(response, 415, { error: 'RECOVERY_CONTENT_TYPE_REQUIRED' }); return; }
648
705
  let message: { id?: unknown; method?: unknown; params?: { name?: unknown; arguments?: unknown } };
649
706
  try { message = JSON.parse(await readBody(request)) as typeof message; } catch { json(response, 400, rpcError(null, -32700, 'Invalid JSON.')); return; }
@@ -675,6 +732,25 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
675
732
  });
676
733
  await new Promise<void>((resolveListen, reject) => { server.once('error', reject); server.listen(gateway.port, gateway.host, () => resolveListen()); });
677
734
  runtimeIdentity = writeRecoveryRuntimeIdentity(config.controllerHome, 'gateway');
735
+ const superviseWatchdog = async () => {
736
+ if (!runtimeIdentity || watchdogRestartInFlight) return;
737
+ const watchdog = observeRecoveryWatchdogHealth(config.controllerHome);
738
+ if (watchdog.ok) return;
739
+ watchdogRestartInFlight = true;
740
+ try {
741
+ const recovery = await restartRecoveryWatchdog(config);
742
+ auditGateway({
743
+ watchdog_supervision: recovery.ok ? 'recovered' : 'failed',
744
+ detail: recovery.detail,
745
+ attempted: recovery.attempted,
746
+ serviceTarget: recovery.serviceTarget,
747
+ });
748
+ } finally {
749
+ watchdogRestartInFlight = false;
750
+ }
751
+ };
752
+ const watchdogSupervisor = setInterval(() => { void superviseWatchdog(); }, 15_000);
753
+ watchdogSupervisor.unref?.();
678
754
  process.stdout.write(JSON.stringify({ status: 'ready', host: gateway.host, port: gateway.port, runtimeIdentity }) + '\n');
679
755
  }
680
756
 
@@ -25,6 +25,8 @@ import {
25
25
  import { isProcessAlive } from '../shared/process-tree';
26
26
  import { FORGE_VERSION } from '../../version';
27
27
  import { initializeStandaloneRecovery, loadRecoveryConfig, type PrimaryConnectorServiceConfig, type PrimaryRuntimeServiceConfig, type PublicTunnelServiceConfig, type RecoveryConfig } from './core';
28
+ import { RECOVERY_GATEWAY_LABEL, RECOVERY_WATCHDOG_LABEL } from './service-labels';
29
+ export { RECOVERY_GATEWAY_LABEL, RECOVERY_WATCHDOG_LABEL } from './service-labels';
28
30
  import {
29
31
  RECOVERY_RELEASE_BINARIES,
30
32
  RECOVERY_RELEASE_ROLE_CANARY_ARG,
@@ -44,9 +46,6 @@ import {
44
46
  type RecoveryRuntimeRole,
45
47
  } from './release';
46
48
 
47
- export const RECOVERY_GATEWAY_LABEL = 'com.moretea.forge-recovery-gateway';
48
- export const RECOVERY_WATCHDOG_LABEL = 'com.moretea.forge-recovery-watchdog';
49
-
50
49
  function defaultRecoveryLaunchctl(args: string[]): ReturnType<LaunchctlCommandRunner> {
51
50
  const result = runProcess('launchctl', args, { timeoutMs: 10_000, maxOutputBytes: 64 * 1024 });
52
51
  return { ok: result.ok, stdout: result.stdout, stderr: result.stderr, exitCode: result.status ?? 0 };
@@ -682,6 +681,12 @@ export function inspectPrimaryConnectorLaunchdContract(
682
681
  return inspectLaunchdRestartContract(service, true);
683
682
  }
684
683
 
684
+ export function inspectPrimaryPublicTunnelLaunchdContract(
685
+ service: Pick<PublicTunnelServiceConfig, 'platform' | 'label' | 'plistPath'>,
686
+ ): RecoveryTunnelLaunchdContract {
687
+ return inspectLaunchdRestartContract(service, true);
688
+ }
689
+
685
690
  export async function installStandaloneRecovery(input: {
686
691
  controllerHome: string;
687
692
  repoRoot: string;
@@ -690,6 +695,7 @@ export async function installStandaloneRecovery(input: {
690
695
  publicMcpUrl?: string;
691
696
  recoveryPublicUrl?: string;
692
697
  recoveryTunnelService?: PublicTunnelServiceConfig;
698
+ primaryPublicTunnelService?: PublicTunnelServiceConfig;
693
699
  primaryRuntimeService?: PrimaryRuntimeServiceConfig;
694
700
  primaryConnectorService?: PrimaryConnectorServiceConfig;
695
701
  stageOnly?: boolean;
@@ -705,6 +711,15 @@ export async function installStandaloneRecovery(input: {
705
711
  throw new Error('RECOVERY_TUNNEL_LAUNCHD_RESTART_CONTRACT_REQUIRED: RunAtLoad=true and KeepAlive=true');
706
712
  }
707
713
  }
714
+ if (input.primaryPublicTunnelService?.platform === 'launchd') {
715
+ const tunnelContract = inspectPrimaryPublicTunnelLaunchdContract(input.primaryPublicTunnelService);
716
+ if (!tunnelContract.plistInstalled) {
717
+ throw new Error(`RECOVERY_PRIMARY_TUNNEL_LAUNCHD_PLIST_MISSING: ${tunnelContract.plistPath}`);
718
+ }
719
+ if (!tunnelContract.restartSafe) {
720
+ throw new Error('RECOVERY_PRIMARY_TUNNEL_LAUNCHD_RESTART_CONTRACT_REQUIRED: RunAtLoad=true and KeepAlive=true or KeepAlive.SuccessfulExit=false');
721
+ }
722
+ }
708
723
  if (input.primaryConnectorService?.platform === 'launchd') {
709
724
  const connectorContract = inspectPrimaryConnectorLaunchdContract(input.primaryConnectorService);
710
725
  if (!connectorContract.plistInstalled) {
@@ -718,6 +733,7 @@ export async function installStandaloneRecovery(input: {
718
733
  ...(input.publicMcpUrl ? { publicMcpUrl: input.publicMcpUrl } : {}),
719
734
  ...(input.recoveryPublicUrl ? { recoveryPublicUrl: input.recoveryPublicUrl } : {}),
720
735
  ...(input.recoveryTunnelService ? { recoveryTunnelService: input.recoveryTunnelService } : {}),
736
+ ...(input.primaryPublicTunnelService ? { primaryPublicTunnelService: input.primaryPublicTunnelService } : {}),
721
737
  ...(input.primaryRuntimeService ? { primaryRuntimeService: input.primaryRuntimeService } : {}),
722
738
  primaryRuntimeSourceRoot: resolve(input.repoRoot),
723
739
  ...(input.primaryConnectorService ? { primaryConnectorService: input.primaryConnectorService } : {}),
@@ -0,0 +1,2 @@
1
+ export const RECOVERY_GATEWAY_LABEL = 'com.moretea.forge-recovery-gateway';
2
+ export const RECOVERY_WATCHDOG_LABEL = 'com.moretea.forge-recovery-watchdog';
@@ -0,0 +1,160 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { writeJsonAtomic } from '../shared/json-files';
4
+ import {
5
+ readCurrentRecoveryRelease,
6
+ readRecoveryRuntimeIdentity,
7
+ recoveryRoot,
8
+ type RecoveryReleaseDescriptor,
9
+ type RecoveryRuntimeIdentity,
10
+ } from './release';
11
+
12
+ export const RECOVERY_WATCHDOG_MAX_PULSE_AGE_MS = 20_000;
13
+ export const RECOVERY_WATCHDOG_MAX_TICK_AGE_MS = 180_000;
14
+
15
+ export interface RecoveryWatchdogHeartbeat {
16
+ schemaVersion: 1;
17
+ pid: number;
18
+ startedAt: string;
19
+ releasePath: string;
20
+ releaseRevision: string;
21
+ sourceCommit: string;
22
+ manifestSha256: string;
23
+ lastPulseAt: string;
24
+ lastTickStartedAt?: string;
25
+ lastTickCompletedAt?: string;
26
+ lastTickFailedAt?: string;
27
+ lastError?: string;
28
+ }
29
+
30
+ export interface RecoveryWatchdogHealth {
31
+ ok: boolean;
32
+ detail: string;
33
+ pulseAgeMs?: number;
34
+ tickAgeMs?: number;
35
+ heartbeat?: RecoveryWatchdogHeartbeat;
36
+ runtimeIdentity?: RecoveryRuntimeIdentity;
37
+ currentReleaseRevision?: string;
38
+ }
39
+
40
+ export function recoveryWatchdogHeartbeatPath(controllerHome: string): string {
41
+ return join(recoveryRoot(controllerHome), 'state', 'watchdog-heartbeat.json');
42
+ }
43
+
44
+ export function createRecoveryWatchdogHeartbeat(
45
+ identity: RecoveryRuntimeIdentity | undefined,
46
+ nowMs = Date.now(),
47
+ ): RecoveryWatchdogHeartbeat {
48
+ const now = new Date(nowMs).toISOString();
49
+ return {
50
+ schemaVersion: 1,
51
+ pid: identity?.pid ?? process.pid,
52
+ startedAt: identity?.startedAt ?? now,
53
+ releasePath: identity?.releasePath ?? 'unmanaged-source',
54
+ releaseRevision: identity?.releaseRevision ?? 'unmanaged-source',
55
+ sourceCommit: identity?.sourceCommit ?? 'unmanaged-source',
56
+ manifestSha256: identity?.manifestSha256 ?? 'unmanaged-source',
57
+ lastPulseAt: now,
58
+ };
59
+ }
60
+
61
+ export function writeRecoveryWatchdogHeartbeat(
62
+ controllerHome: string,
63
+ heartbeat: RecoveryWatchdogHeartbeat,
64
+ ): RecoveryWatchdogHeartbeat {
65
+ writeJsonAtomic(recoveryWatchdogHeartbeatPath(controllerHome), heartbeat);
66
+ return heartbeat;
67
+ }
68
+
69
+ export function readRecoveryWatchdogHeartbeat(controllerHome: string): RecoveryWatchdogHeartbeat | undefined {
70
+ try {
71
+ const parsed = JSON.parse(readFileSync(recoveryWatchdogHeartbeatPath(controllerHome), 'utf8')) as RecoveryWatchdogHeartbeat;
72
+ if (
73
+ parsed.schemaVersion !== 1
74
+ || !Number.isInteger(parsed.pid)
75
+ || typeof parsed.lastPulseAt !== 'string'
76
+ || typeof parsed.releaseRevision !== 'string'
77
+ || typeof parsed.manifestSha256 !== 'string'
78
+ ) return undefined;
79
+ return parsed;
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ }
84
+
85
+ function pidExists(pid: number): boolean {
86
+ if (!Number.isInteger(pid) || pid <= 0) return false;
87
+ try {
88
+ process.kill(pid, 0);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ function latestTerminalTickAt(heartbeat: RecoveryWatchdogHeartbeat): number {
96
+ return Math.max(
97
+ heartbeat.lastTickCompletedAt ? Date.parse(heartbeat.lastTickCompletedAt) : 0,
98
+ heartbeat.lastTickFailedAt ? Date.parse(heartbeat.lastTickFailedAt) : 0,
99
+ );
100
+ }
101
+
102
+ export function evaluateRecoveryWatchdogHealth(input: {
103
+ heartbeat?: RecoveryWatchdogHeartbeat;
104
+ runtimeIdentity?: RecoveryRuntimeIdentity;
105
+ currentRelease?: Pick<RecoveryReleaseDescriptor, 'releasePath' | 'releaseRevision' | 'sourceCommit' | 'manifestSha256'>;
106
+ nowMs?: number;
107
+ maxPulseAgeMs?: number;
108
+ maxTickAgeMs?: number;
109
+ pidAlive?: (pid: number) => boolean;
110
+ }): RecoveryWatchdogHealth {
111
+ const heartbeat = input.heartbeat;
112
+ const runtimeIdentity = input.runtimeIdentity;
113
+ const current = input.currentRelease;
114
+ if (!current) return { ok: false, detail: 'current immutable Recovery release is unavailable' };
115
+ if (!runtimeIdentity) return { ok: false, detail: 'Recovery Watchdog runtime identity is unavailable', currentReleaseRevision: current.releaseRevision };
116
+ if (!heartbeat) return { ok: false, detail: 'Recovery Watchdog heartbeat is unavailable', runtimeIdentity, currentReleaseRevision: current.releaseRevision };
117
+ if (heartbeat.pid !== runtimeIdentity.pid) {
118
+ return { ok: false, detail: 'Recovery Watchdog heartbeat PID does not match runtime identity', heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
119
+ }
120
+ if (
121
+ runtimeIdentity.releasePath !== current.releasePath
122
+ || runtimeIdentity.releaseRevision !== current.releaseRevision
123
+ || runtimeIdentity.sourceCommit !== current.sourceCommit
124
+ || runtimeIdentity.manifestSha256 !== current.manifestSha256
125
+ || heartbeat.releasePath !== current.releasePath
126
+ || heartbeat.releaseRevision !== current.releaseRevision
127
+ || heartbeat.sourceCommit !== current.sourceCommit
128
+ || heartbeat.manifestSha256 !== current.manifestSha256
129
+ ) {
130
+ return { ok: false, detail: 'Recovery Watchdog is not running the current immutable Recovery release', heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
131
+ }
132
+ if (!(input.pidAlive ?? pidExists)(runtimeIdentity.pid)) {
133
+ return { ok: false, detail: 'Recovery Watchdog process is not alive', heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
134
+ }
135
+ const now = input.nowMs ?? Date.now();
136
+ const pulseAt = Date.parse(heartbeat.lastPulseAt);
137
+ const pulseAgeMs = Number.isFinite(pulseAt) ? Math.max(0, now - pulseAt) : Number.POSITIVE_INFINITY;
138
+ const maxPulseAgeMs = Math.max(1_000, input.maxPulseAgeMs ?? RECOVERY_WATCHDOG_MAX_PULSE_AGE_MS);
139
+ if (pulseAgeMs > maxPulseAgeMs) {
140
+ return { ok: false, detail: 'Recovery Watchdog heartbeat is stale', pulseAgeMs, heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
141
+ }
142
+ const tickStartedAt = heartbeat.lastTickStartedAt ? Date.parse(heartbeat.lastTickStartedAt) : 0;
143
+ const tickTerminalAt = latestTerminalTickAt(heartbeat);
144
+ if (tickStartedAt > tickTerminalAt) {
145
+ const tickAgeMs = Math.max(0, now - tickStartedAt);
146
+ const maxTickAgeMs = Math.max(maxPulseAgeMs, input.maxTickAgeMs ?? RECOVERY_WATCHDOG_MAX_TICK_AGE_MS);
147
+ if (tickAgeMs > maxTickAgeMs) {
148
+ return { ok: false, detail: 'Recovery Watchdog tick is stuck beyond its bounded recovery window', pulseAgeMs, tickAgeMs, heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
149
+ }
150
+ }
151
+ return { ok: true, detail: 'Recovery Watchdog heartbeat, PID, and immutable release identity are current', pulseAgeMs, heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
152
+ }
153
+
154
+ export function observeRecoveryWatchdogHealth(controllerHome: string): RecoveryWatchdogHealth {
155
+ return evaluateRecoveryWatchdogHealth({
156
+ heartbeat: readRecoveryWatchdogHeartbeat(controllerHome),
157
+ runtimeIdentity: readRecoveryRuntimeIdentity(controllerHome, 'watchdog'),
158
+ currentRelease: readCurrentRecoveryRelease(controllerHome),
159
+ });
160
+ }