@moretea-labs/forge 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +39 -0
  3. package/CLAUDE.md +4 -0
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +1 -1
  6. package/SKILL.md +2 -2
  7. package/THIRD_PARTY_NOTICES.md +1 -1
  8. package/agents/openai.yaml +1 -1
  9. package/assets/hooks/hook-input.sh +11 -8
  10. package/assets/hooks/lib/session-state.sh +10 -7
  11. package/assets/hooks/lib/workflow-state.sh +13 -177
  12. package/assets/hooks/post-tool-observer.sh +8 -2
  13. package/assets/hooks/prompt-guard.sh +5 -81
  14. package/assets/hooks/session-start-context.sh +56 -1
  15. package/assets/partials/05-workflow.partial.md +1 -1
  16. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  17. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  18. package/assets/plugin-registry.v1.json +3 -3
  19. package/assets/reference-configs/document-generation.md +1 -1
  20. package/assets/reference-configs/harness-overview.md +1 -1
  21. package/assets/reference-configs/sprint-contracts.md +3 -3
  22. package/assets/skill-version.json +2 -2
  23. package/assets/templates/contract.template.md +2 -7
  24. package/assets/templates/helpers/capture-plan.sh +2 -2
  25. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  26. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  27. package/assets/templates/helpers/contract-worktree.sh +1 -21
  28. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  29. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  32. package/assets/templates/helpers/new-plan.sh +1 -1
  33. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  34. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/helpers/workflow-contract.ts +5 -1
  38. package/assets/templates/plan.template.md +1 -1
  39. package/assets/templates/review.template.md +9 -34
  40. package/assets/workflow-contract.v1.json +10 -19
  41. package/docs/README.md +10 -10
  42. package/docs/architecture/CURRENT.md +178 -0
  43. package/docs/architecture/EVOLUTION.md +42 -0
  44. package/docs/architecture/current/README.md +4 -156
  45. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  46. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  47. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  48. package/docs/architecture/history/README.md +7 -0
  49. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  50. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  51. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  52. package/docs/architecture/history.md +3 -34
  53. package/docs/architecture/index.md +15 -98
  54. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  55. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  56. package/docs/architecture/versions/1.6.md +38 -0
  57. package/docs/architecture/versions/1.7.md +41 -0
  58. package/docs/forge-chatgpt-browser-engine.md +2 -2
  59. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  60. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  61. package/docs/forge-github-issue-launcher.md +1 -1
  62. package/docs/forge-plugin-management.md +1 -1
  63. package/docs/forge-runtime-self-healing-loop.md +1 -1
  64. package/docs/operations/controller-browser-plugin.md +26 -6
  65. package/docs/operations/controller-performance-and-502.md +143 -0
  66. package/docs/operations/controller-reliability-runbook.md +1 -1
  67. package/docs/operations/features.md +2 -2
  68. package/docs/operations/github-repository.md +1 -1
  69. package/docs/operations/homebrew.md +2 -2
  70. package/docs/operations/mcp-tool-exposure.md +5 -3
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Integrations.md +2 -2
  74. package/docs/wiki/Work-Lifecycle.md +1 -1
  75. package/package.json +33 -11
  76. package/scripts/activate-source-baseline.ts +17 -13
  77. package/scripts/architecture-event.ts +2 -820
  78. package/scripts/architecture-queue.sh +2 -645
  79. package/scripts/archive-architecture-request.sh +2 -242
  80. package/scripts/archive-workflow.sh +1 -291
  81. package/scripts/benchmark-background-check-overlap.ts +228 -0
  82. package/scripts/benchmark-mcp-host.sh +117 -0
  83. package/scripts/benchmark-mcp-transport.ts +170 -0
  84. package/scripts/benchmark-quality-harness.ts +146 -0
  85. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  86. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  87. package/scripts/benchmark-typescript-navigation.ts +186 -0
  88. package/scripts/benchmark-work-efficiency.ts +334 -0
  89. package/scripts/capability-config.ts +2 -392
  90. package/scripts/capability-resolver.ts +2 -503
  91. package/scripts/capture-plan.sh +1 -332
  92. package/scripts/check-agent-tooling.sh +2 -1558
  93. package/scripts/check-architecture-sync.sh +2 -371
  94. package/scripts/check-brain-manifest.sh +2 -253
  95. package/scripts/check-context-files.sh +2 -47
  96. package/scripts/check-deploy-sql-order.sh +2 -115
  97. package/scripts/check-mcp-compatibility.ts +14 -0
  98. package/scripts/check-runtime-architecture.mjs +349 -79
  99. package/scripts/check-skill-version.ts +3 -191
  100. package/scripts/check-source-duplication.mjs +61 -0
  101. package/scripts/check-stable-baseline.ts +92 -0
  102. package/scripts/check-task-sync.sh +2 -63
  103. package/scripts/check-task-workflow.sh +2 -1000
  104. package/scripts/codex-handoff-resume.sh +2 -267
  105. package/scripts/context-contract-sync.sh +2 -514
  106. package/scripts/contract-run.ts +2 -463
  107. package/scripts/contract-worktree.sh +1 -722
  108. package/scripts/ensure-task-workflow.sh +1 -1225
  109. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  110. package/scripts/inspect-project-state.ts +4 -279
  111. package/scripts/install-standalone-recovery.ts +24 -0
  112. package/scripts/lib/project-init-lib.sh +22 -46
  113. package/scripts/lib/run-forge-helper.sh +19 -0
  114. package/scripts/lib/run-forge-helper.ts +26 -0
  115. package/scripts/migrate-project-template.sh +1 -1189
  116. package/scripts/migrate-workflow-docs.ts +4 -412
  117. package/scripts/new-plan.sh +1 -180
  118. package/scripts/new-spec.sh +1 -47
  119. package/scripts/new-sprint.sh +1 -46
  120. package/scripts/plan-to-todo.sh +1 -796
  121. package/scripts/prepare-codex-handoff.sh +2 -201
  122. package/scripts/prepare-handoff.sh +1 -78
  123. package/scripts/refresh-current-status.sh +2 -467
  124. package/scripts/run-governed-gate.ts +5 -1
  125. package/scripts/select-agent-context-blocks.sh +2 -64
  126. package/scripts/ship-worktrees.sh +1 -558
  127. package/scripts/smoke-ios-physical-device.ts +4 -2
  128. package/scripts/smoke-runtime-recovery.ts +1 -35
  129. package/scripts/smoke-schedule-engine.ts +1 -20
  130. package/scripts/sprint-backlog.sh +1 -834
  131. package/scripts/stage-runtime-release.ts +2 -1
  132. package/scripts/summarize-failures.sh +2 -123
  133. package/scripts/switch-plan.sh +1 -150
  134. package/scripts/sync-brain-docs.sh +2 -379
  135. package/scripts/sync-generated-authority.mjs +175 -0
  136. package/scripts/verify-contract.sh +2 -703
  137. package/scripts/verify-forge-runtime.sh +0 -1
  138. package/scripts/verify-sprint.sh +1 -677
  139. package/scripts/workflow-contract.ts +4 -205
  140. package/scripts/workstream-sync.sh +2 -261
  141. package/src/cli/agent-jobs/executor-health.ts +1 -1
  142. package/src/cli/commands/init-hook.ts +3 -3
  143. package/src/cli/commands/mcp.ts +9 -5
  144. package/src/cli/commands/plugin.ts +12 -3
  145. package/src/cli/commands/recovery.ts +25 -3
  146. package/src/cli/commands/runtime.ts +55 -3
  147. package/src/cli/commands/security.ts +12 -1
  148. package/src/cli/commands/setup-profile.ts +31 -21
  149. package/src/cli/controller/check-runner.ts +113 -5
  150. package/src/cli/controller/composite-result.ts +1 -1
  151. package/src/cli/controller/context/focus.ts +51 -0
  152. package/src/cli/controller/context/instruction-context.ts +114 -0
  153. package/src/cli/controller/context/known-paths.ts +76 -0
  154. package/src/cli/controller/context/query-planning.ts +110 -0
  155. package/src/cli/controller/context/rule-resolution.ts +532 -0
  156. package/src/cli/controller/context/source-materializer.ts +278 -0
  157. package/src/cli/controller/context/structural.ts +16 -0
  158. package/src/cli/controller/context/types.ts +286 -0
  159. package/src/cli/controller/context-pack.ts +828 -487
  160. package/src/cli/controller/launch-agents.ts +77 -2
  161. package/src/cli/controller/operational-plan.ts +2 -2
  162. package/src/cli/controller/work-mode.ts +6 -4
  163. package/src/cli/editing/edit-session.ts +60 -7
  164. package/src/cli/github/contracts.ts +16 -0
  165. package/src/cli/github/github.ts +135 -16
  166. package/src/cli/github/session.ts +2 -1
  167. package/src/cli/installer/managed-entries.ts +15 -1
  168. package/src/cli/local-bridge/console-automations.ts +54 -78
  169. package/src/cli/local-bridge/console-view-models.ts +1 -35
  170. package/src/cli/local-bridge/facade-api.ts +37 -264
  171. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  172. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  173. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  174. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  175. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  176. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  177. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  178. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  179. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  180. package/src/cli/local-bridge/job-store.ts +1 -490
  181. package/src/cli/local-bridge/server.ts +2 -515
  182. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  183. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  184. package/src/cli/mcp/auth.ts +5 -5
  185. package/src/cli/mcp/instructions.ts +3 -2
  186. package/src/cli/mcp/legacy-tool-service.ts +10 -222
  187. package/src/cli/mcp/multi-repository.ts +16 -5
  188. package/src/cli/mcp/oauth.ts +175 -26
  189. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  190. package/src/cli/mcp/repository-tools.ts +282 -146
  191. package/src/cli/mcp/server.ts +586 -116
  192. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  193. package/src/cli/mcp/tool-contract.ts +14 -0
  194. package/src/cli/mcp/toolset-selection.ts +1 -1
  195. package/src/cli/mcp/toolset.ts +44 -7
  196. package/src/cli/mcp/transports/http.ts +124 -14
  197. package/src/cli/mcp/transports/session-registry.ts +43 -5
  198. package/src/cli/mcp/types.ts +3 -3
  199. package/src/cli/repositories/command-classifier.ts +103 -21
  200. package/src/cli/repositories/command-executor.ts +78 -526
  201. package/src/cli/repositories/command-process.ts +108 -0
  202. package/src/cli/repositories/command-scope.ts +99 -41
  203. package/src/cli/repositories/controller-home.ts +7 -8
  204. package/src/cli/repositories/git-command-executor.ts +2 -0
  205. package/src/cli/repositories/locks.ts +53 -16
  206. package/src/cli/repositories/repository-snapshot.ts +124 -0
  207. package/src/cli/repositories/selected-path-actions.ts +17 -9
  208. package/src/cli/repositories/structured-git.ts +118 -6
  209. package/src/cli/repository/inspector.ts +563 -65
  210. package/src/cli/repository/session-cache.ts +58 -17
  211. package/src/cli/v81-entry.ts +1 -4
  212. package/src/effects/fs-transaction.ts +85 -17
  213. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  214. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  215. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  216. package/src/runtime/context/lsp-client.ts +283 -0
  217. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  218. package/src/runtime/context/semantic-navigation.ts +223 -0
  219. package/src/runtime/context/swift-navigation.ts +565 -0
  220. package/src/runtime/context/typescript-navigation.ts +226 -0
  221. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  222. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
  223. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  224. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  225. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  226. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  227. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  228. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  229. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  230. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  231. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  232. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  233. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  234. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  235. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  236. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  237. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  238. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  239. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  240. package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
  241. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
  242. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  243. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  244. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  245. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
  246. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  247. package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
  248. package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
  249. package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
  250. package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
  251. package/src/runtime/control-plane/facade/index.ts +3 -0
  252. package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
  253. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  254. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  255. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  256. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  257. package/src/runtime/control-plane/facade/types.ts +90 -10
  258. package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
  259. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  260. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  261. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  262. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  263. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  264. package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
  265. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  266. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  267. package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
  268. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  269. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  270. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  271. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  272. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  273. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  274. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  275. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  276. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  277. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  278. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
  279. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
  280. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
  281. package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
  282. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  283. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  284. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  285. package/src/runtime/control-plane/release-retention.ts +62 -2
  286. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  287. package/src/runtime/control-plane/routing/route-policy.ts +24 -21
  288. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  289. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  290. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  291. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  292. package/src/runtime/diagnostics/performance.ts +24 -1
  293. package/src/runtime/evidence/event-ledger.ts +1 -1
  294. package/src/runtime/evidence/process-check-execution.ts +18 -0
  295. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  296. package/src/runtime/execution/jobs/store.ts +58 -4
  297. package/src/runtime/execution/jobs/types.ts +1 -1
  298. package/src/runtime/execution/managed-workspace.ts +203 -1
  299. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  300. package/src/runtime/execution/process-runtime/check-result.ts +88 -0
  301. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  302. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  303. package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
  304. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  305. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
  306. package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
  307. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  308. package/src/runtime/execution/process-runtime/runtime.ts +53 -3
  309. package/src/runtime/execution/process-runtime/store.ts +1 -0
  310. package/src/runtime/execution/process-runtime/types.ts +4 -12
  311. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  312. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  313. package/src/runtime/execution/work-activity.ts +21 -0
  314. package/src/runtime/execution/workers/executor.ts +18 -61
  315. package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
  316. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  317. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  318. package/src/runtime/gateway/mcp/router.ts +186 -84
  319. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
  320. package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
  321. package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
  322. package/src/runtime/plugins/browser-adapter.ts +2662 -829
  323. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  324. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  325. package/src/runtime/plugins/browser-handoff.ts +110 -5
  326. package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
  327. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  328. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  329. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  330. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  331. package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
  332. package/src/runtime/plugins/browser-runtime.ts +193 -0
  333. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  334. package/src/runtime/plugins/browser-session-store.ts +121 -0
  335. package/src/runtime/plugins/browser-session-types.ts +98 -0
  336. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  337. package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
  338. package/src/runtime/plugins/external-adapter.ts +358 -3
  339. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  340. package/src/runtime/plugins/first-party-registry.ts +2 -0
  341. package/src/runtime/plugins/github-adapter.ts +125 -0
  342. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  343. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  344. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  345. package/src/runtime/plugins/ios-adapter.ts +106 -16
  346. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  347. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  348. package/src/runtime/plugins/lightweight-action.ts +153 -0
  349. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  350. package/src/runtime/plugins/macos-capability-broker.ts +47 -9
  351. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  352. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  353. package/src/runtime/plugins/resend-adapter.ts +1 -1
  354. package/src/runtime/plugins/store.ts +330 -33
  355. package/src/runtime/plugins/types.ts +38 -3
  356. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  357. package/src/runtime/projections/git-status-sampler.ts +16 -16
  358. package/src/runtime/projections/materialized-view.ts +28 -10
  359. package/src/runtime/recovery/maintenance-executor.ts +426 -25
  360. package/src/runtime/root/mcp-transport.ts +180 -10
  361. package/src/runtime/root/package-connector-service.ts +230 -10
  362. package/src/runtime/root/package-runtime-release.ts +143 -17
  363. package/src/runtime/root/package-runtime-service.ts +461 -22
  364. package/src/runtime/root/release-manifest.ts +144 -18
  365. package/src/runtime/root/release-materialize.ts +125 -32
  366. package/src/runtime/root/release-store.ts +48 -11
  367. package/src/runtime/root/runtime.ts +23 -5
  368. package/src/runtime/root/service.ts +100 -29
  369. package/src/runtime/root/types.ts +12 -0
  370. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  371. package/src/runtime/shared/trusted-executable.ts +62 -0
  372. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  373. package/src/runtime/standalone-recovery/core.ts +1079 -240
  374. package/src/runtime/standalone-recovery/entry.ts +100 -24
  375. package/src/runtime/standalone-recovery/installer.ts +20 -4
  376. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  377. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  378. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  379. package/src/runtime/workflow/schedules/engine.ts +364 -202
  380. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  381. package/src/runtime/workflow/schedules/settlement.ts +64 -17
  382. package/src/runtime/workflow/schedules/store.ts +209 -19
  383. package/src/runtime/workflow/schedules/types.ts +2 -4
  384. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  385. package/src/testing/test-governance.ts +8 -40
  386. package/assets/reference-configs/heartbeat-triage.md +0 -57
  387. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  388. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  389. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  390. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  391. package/docs/architecture/current/architecture-invariants.md +0 -331
  392. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  393. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  394. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  395. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  396. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  397. package/docs/architecture/current/entity-model.md +0 -612
  398. package/docs/architecture/current/external-plugin-governance.md +0 -396
  399. package/docs/architecture/current/failure-recovery.md +0 -447
  400. package/docs/architecture/current/governance.md +0 -213
  401. package/docs/architecture/current/human-interaction-plane.md +0 -87
  402. package/docs/architecture/current/implementation-status.md +0 -128
  403. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  404. package/docs/architecture/current/legacy-governance.md +0 -58
  405. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  406. package/docs/architecture/current/migration-roadmap.md +0 -105
  407. package/docs/architecture/current/multi-repository-execution.md +0 -371
  408. package/docs/architecture/current/operations-runbook.md +0 -45
  409. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  410. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  411. package/docs/architecture/current/repository-access-modes.md +0 -102
  412. package/docs/architecture/current/route-policy.md +0 -15
  413. package/docs/architecture/current/route-session-concurrency.md +0 -85
  414. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  415. package/docs/architecture/current/runtime-directory-map.md +0 -49
  416. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  417. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  418. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  419. package/docs/architecture/current/system-overview.md +0 -169
  420. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  421. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  422. package/docs/architecture/current/thin-harness-v1.md +0 -321
  423. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  424. package/docs/forge-autonomous-goal-loop.md +0 -9
  425. package/docs/forge-provider-configuration.md +0 -7
  426. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  427. package/docs/operations/gmail-assistant-routines.md +0 -186
  428. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  429. package/docs/operations/provider-configuration.md +0 -31
  430. package/scripts/harness-trace-grade.sh +0 -150
  431. package/scripts/heartbeat-triage.sh +0 -361
  432. package/scripts/maintenance-triage.sh +0 -167
  433. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  434. package/src/cli/repositories/goal-registry.ts +0 -239
  435. package/src/runtime/assistant/action-proposals.ts +0 -279
  436. package/src/runtime/assistant/intent.ts +0 -395
  437. package/src/runtime/assistant/model-provider.ts +0 -460
  438. package/src/runtime/assistant/openapi.ts +0 -223
  439. package/src/runtime/assistant/policy.ts +0 -89
  440. package/src/runtime/assistant/readiness.ts +0 -258
  441. package/src/runtime/assistant/routine-runtime.ts +0 -477
  442. package/src/runtime/assistant/schedule-binding.ts +0 -171
  443. package/src/runtime/assistant/standing-grants.ts +0 -375
  444. package/src/runtime/assistant/store.ts +0 -243
  445. package/src/runtime/assistant/types.ts +0 -135
  446. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  447. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  448. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  449. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  450. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  451. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  452. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  453. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  454. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  455. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  456. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  457. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  458. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  459. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  460. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  461. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  462. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  463. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  464. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  465. package/src/runtime/workflow/findings/store.ts +0 -204
  466. package/src/runtime/workflow/findings/types.ts +0 -58
  467. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  468. package/src/runtime/workflow/portfolio/store.ts +0 -185
  469. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -1,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 {
@@ -462,7 +508,7 @@ export async function dispatchRecoveryTool(config: RecoveryConfig, name: string,
462
508
  }
463
509
  case 'restart_primary_connector': {
464
510
  if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
465
- return restartPrimaryConnector(config);
511
+ return restartPrimaryConnector(config, { requestId: `recovery-gateway:${args.request_id}` });
466
512
  }
467
513
  case 'recover_primary_runtime': {
468
514
  if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
@@ -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 };
@@ -341,7 +340,7 @@ function recoveryPlist(input: {
341
340
  }): string {
342
341
  const xml = (value: string) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
343
342
  const argumentsList = [
344
- '/usr/bin/env', '-i', 'PATH=/usr/bin:/bin:/usr/sbin:/sbin', `FORGE_BUILD_VERSION=${FORGE_VERSION}`,
343
+ '/usr/bin/env', '-i', 'PATH=/usr/bin:/bin:/usr/sbin:/sbin', `FORGE_BUILD_VERSION=${FORGE_VERSION}`, `FORGE_CONNECTOR_EXECUTABLE=${process.execPath}`,
345
344
  input.executable, input.command, '--controller-home', input.controllerHome,
346
345
  ];
347
346
  return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${xml(input.label)}</string><key>ProgramArguments</key><array>${argumentsList.map((argument) => `<string>${xml(argument)}</string>`).join('')}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer><key>StandardOutPath</key><string>${xml(input.logPath)}</string><key>StandardErrorPath</key><string>${xml(input.logPath)}</string></dict></plist>\n`;
@@ -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
+ }
@@ -95,10 +95,70 @@ async function browserAction(
95
95
  });
96
96
  }
97
97
 
98
+ interface ScheduledBrowserSessionMetadata {
99
+ url?: string;
100
+ ownership?: string;
101
+ provider?: string;
102
+ activeMode?: string;
103
+ }
104
+
105
+ export type ScheduledBrowserProbeNavigationAction = 'navigate' | 'reload' | 'wait_for_load_state';
106
+
107
+ export function scheduledBrowserProbeNavigationAction(
108
+ ownership: string | undefined,
109
+ hasUrl: boolean,
110
+ ): ScheduledBrowserProbeNavigationAction {
111
+ if (ownership === 'user_owned') return 'wait_for_load_state';
112
+ return hasUrl ? 'navigate' : 'reload';
113
+ }
114
+
115
+ export function scheduledBrowserProbeManagedRehydrateIntent(
116
+ savedSession: Pick<ScheduledBrowserSessionMetadata, 'ownership' | 'provider' | 'activeMode'> | undefined,
117
+ ): Record<string, true> {
118
+ return savedSession?.ownership === 'plugin_owned'
119
+ && savedSession.provider === 'playwright-persistent-context'
120
+ && savedSession.activeMode === 'managed_persistent'
121
+ ? { __forge_allow_managed_session_rehydrate: true }
122
+ : {};
123
+ }
124
+
125
+ function comparableProbeUrl(value: string): string {
126
+ try {
127
+ const parsed = new URL(value);
128
+ parsed.hash = '';
129
+ return parsed.toString();
130
+ } catch {
131
+ return value;
132
+ }
133
+ }
134
+
135
+ async function scheduledBrowserSessionMetadata(
136
+ controllerHome: string,
137
+ repository: RepositoryRecord,
138
+ occurrenceId: string,
139
+ sessionId: string,
140
+ ): Promise<ScheduledBrowserSessionMetadata | undefined> {
141
+ const inventory = await browserAction(controllerHome, repository, occurrenceId, 'list_sessions', {});
142
+ const sessions = Array.isArray(inventory.sessions) ? inventory.sessions : [];
143
+ const saved = sessions
144
+ .map((entry) => recordValue(entry))
145
+ .find((entry) => stringValue(entry.sessionId) === sessionId);
146
+ if (!saved) return undefined;
147
+ const browser = recordValue(saved.browser);
148
+ const tab = recordValue(browser.tab);
149
+ return {
150
+ url: stringValue(saved.url),
151
+ ownership: stringValue(tab.ownership),
152
+ provider: stringValue(browser.provider),
153
+ activeMode: stringValue(browser.activeMode),
154
+ };
155
+ }
156
+
98
157
  /**
99
158
  * Bounded read-only browser observation for Schedule. It never interprets page
100
- * meaning: it only refreshes/navigates, extracts bounded text, projects matching
101
- * lines, detects configured login markers, and returns a stable fingerprint.
159
+ * meaning: it only refreshes/navigates plugin-owned targets, observes user-owned
160
+ * targets without mutating them, extracts bounded text, projects matching lines,
161
+ * detects configured login markers, and returns a stable fingerprint.
102
162
  */
103
163
  export async function executeScheduledBrowserProbe(input: {
104
164
  controllerHome: string;
@@ -112,20 +172,38 @@ export async function executeScheduledBrowserProbe(input: {
112
172
 
113
173
  const waitUntil = stringValue(input.args.wait_until) ?? 'domcontentloaded';
114
174
  const timeoutMs = boundedNumber(input.args.timeout_ms, 60_000, 1_000, 120_000);
175
+ const savedSession = requestedSessionId
176
+ ? await scheduledBrowserSessionMetadata(input.controllerHome, input.repository, input.occurrenceId, requestedSessionId)
177
+ : undefined;
178
+ const navigationAction = scheduledBrowserProbeNavigationAction(savedSession?.ownership, Boolean(url));
179
+ const managedRehydrateIntent = scheduledBrowserProbeManagedRehydrateIntent(savedSession);
115
180
  let navigation: Record<string, unknown>;
116
- if (url) {
181
+ if (navigationAction === 'wait_for_load_state') {
182
+ if (!savedSession) throw new Error('SCHEDULE_BROWSER_PROBE_USER_SESSION_STATE_UNAVAILABLE');
183
+ if (url && savedSession.url && comparableProbeUrl(url) !== comparableProbeUrl(savedSession.url)) {
184
+ throw new Error('SCHEDULE_BROWSER_PROBE_USER_SESSION_NAVIGATION_REFUSED');
185
+ }
186
+ navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'wait_for_load_state', {
187
+ session_id: requestedSessionId!,
188
+ state: waitUntil,
189
+ timeout_ms: timeoutMs,
190
+ ...managedRehydrateIntent,
191
+ });
192
+ } else if (navigationAction === 'navigate') {
117
193
  navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'navigate', {
118
194
  ...(requestedSessionId ? { session_id: requestedSessionId } : {}),
119
195
  url,
120
196
  wait_until: waitUntil,
121
197
  timeout_ms: timeoutMs,
122
198
  retries: 1,
199
+ ...managedRehydrateIntent,
123
200
  });
124
201
  } else {
125
202
  navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'reload', {
126
203
  session_id: requestedSessionId!,
127
204
  wait_until: waitUntil,
128
205
  timeout_ms: timeoutMs,
206
+ ...managedRehydrateIntent,
129
207
  });
130
208
  }
131
209