@moretea-labs/forge 1.6.0 → 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 (396) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -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/03-orchestration.partial.md +1 -1
  17. package/assets/plugin-registry.v1.json +1 -1
  18. package/assets/reference-configs/document-generation.md +1 -1
  19. package/assets/reference-configs/harness-overview.md +1 -1
  20. package/assets/reference-configs/sprint-contracts.md +3 -3
  21. package/assets/skill-version.json +2 -2
  22. package/assets/templates/contract.template.md +2 -7
  23. package/assets/templates/helpers/capture-plan.sh +2 -2
  24. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  25. package/assets/templates/helpers/contract-worktree.sh +1 -21
  26. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  27. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  28. package/assets/templates/helpers/new-plan.sh +1 -1
  29. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  30. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  31. package/assets/templates/helpers/verify-contract.sh +1 -111
  32. package/assets/templates/helpers/verify-sprint.sh +3 -180
  33. package/assets/templates/plan.template.md +1 -1
  34. package/assets/templates/review.template.md +9 -34
  35. package/assets/workflow-contract.v1.json +10 -19
  36. package/docs/README.md +10 -10
  37. package/docs/architecture/CURRENT.md +170 -0
  38. package/docs/architecture/EVOLUTION.md +42 -0
  39. package/docs/architecture/current/README.md +4 -156
  40. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  41. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  42. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  43. package/docs/architecture/history/README.md +7 -0
  44. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  45. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  46. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  47. package/docs/architecture/history.md +3 -34
  48. package/docs/architecture/index.md +15 -98
  49. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  50. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  51. package/docs/architecture/versions/1.6.md +38 -0
  52. package/docs/architecture/versions/1.7.md +41 -0
  53. package/docs/forge-chatgpt-browser-engine.md +2 -2
  54. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  55. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  56. package/docs/forge-github-issue-launcher.md +1 -1
  57. package/docs/forge-runtime-self-healing-loop.md +1 -1
  58. package/docs/operations/controller-browser-plugin.md +26 -6
  59. package/docs/operations/controller-performance-and-502.md +124 -0
  60. package/docs/operations/controller-reliability-runbook.md +1 -1
  61. package/docs/operations/features.md +2 -2
  62. package/docs/operations/github-repository.md +1 -1
  63. package/docs/operations/homebrew.md +2 -2
  64. package/docs/operations/mcp-tool-exposure.md +5 -3
  65. package/docs/wiki/Architecture.md +6 -6
  66. package/docs/wiki/Implementation.md +2 -2
  67. package/docs/wiki/Integrations.md +2 -2
  68. package/docs/wiki/Work-Lifecycle.md +1 -1
  69. package/package.json +29 -10
  70. package/scripts/activate-source-baseline.ts +17 -13
  71. package/scripts/benchmark-background-check-overlap.ts +228 -0
  72. package/scripts/benchmark-mcp-host.sh +117 -0
  73. package/scripts/benchmark-mcp-transport.ts +170 -0
  74. package/scripts/benchmark-quality-harness.ts +146 -0
  75. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  76. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  77. package/scripts/benchmark-typescript-navigation.ts +186 -0
  78. package/scripts/benchmark-work-efficiency.ts +334 -0
  79. package/scripts/capture-plan.sh +3 -3
  80. package/scripts/check-agent-tooling.sh +8 -35
  81. package/scripts/check-architecture-sync.sh +17 -86
  82. package/scripts/check-mcp-compatibility.ts +14 -0
  83. package/scripts/check-runtime-architecture.mjs +107 -64
  84. package/scripts/check-stable-baseline.ts +92 -0
  85. package/scripts/check-task-workflow.sh +4 -4
  86. package/scripts/contract-worktree.sh +1 -21
  87. package/scripts/ensure-task-workflow.sh +22 -55
  88. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  89. package/scripts/install-standalone-recovery.ts +24 -0
  90. package/scripts/lib/project-init-lib.sh +15 -44
  91. package/scripts/migrate-project-template.sh +23 -1179
  92. package/scripts/new-plan.sh +1 -1
  93. package/scripts/plan-to-todo.sh +11 -36
  94. package/scripts/run-governed-gate.ts +2 -1
  95. package/scripts/ship-worktrees.sh +2 -29
  96. package/scripts/smoke-ios-physical-device.ts +4 -2
  97. package/scripts/smoke-runtime-recovery.ts +1 -35
  98. package/scripts/smoke-schedule-engine.ts +1 -20
  99. package/scripts/verify-contract.sh +1 -111
  100. package/scripts/verify-forge-runtime.sh +0 -1
  101. package/scripts/verify-sprint.sh +3 -180
  102. package/scripts/workflow-contract.ts +1 -5
  103. package/src/cli/commands/init-hook.ts +3 -3
  104. package/src/cli/commands/mcp.ts +9 -5
  105. package/src/cli/commands/plugin.ts +1 -1
  106. package/src/cli/commands/recovery.ts +25 -3
  107. package/src/cli/commands/runtime.ts +55 -3
  108. package/src/cli/commands/security.ts +12 -1
  109. package/src/cli/commands/setup-profile.ts +31 -21
  110. package/src/cli/controller/check-runner.ts +106 -3
  111. package/src/cli/controller/composite-result.ts +1 -1
  112. package/src/cli/controller/context/focus.ts +51 -0
  113. package/src/cli/controller/context/instruction-context.ts +114 -0
  114. package/src/cli/controller/context/known-paths.ts +76 -0
  115. package/src/cli/controller/context/query-planning.ts +110 -0
  116. package/src/cli/controller/context/rule-resolution.ts +532 -0
  117. package/src/cli/controller/context/source-materializer.ts +278 -0
  118. package/src/cli/controller/context/structural.ts +16 -0
  119. package/src/cli/controller/context/types.ts +286 -0
  120. package/src/cli/controller/context-pack.ts +785 -469
  121. package/src/cli/controller/launch-agents.ts +77 -2
  122. package/src/cli/controller/operational-plan.ts +2 -2
  123. package/src/cli/controller/work-mode.ts +6 -4
  124. package/src/cli/editing/edit-session.ts +60 -7
  125. package/src/cli/github/github.ts +133 -0
  126. package/src/cli/installer/managed-entries.ts +15 -1
  127. package/src/cli/local-bridge/console-automations.ts +54 -78
  128. package/src/cli/local-bridge/console-view-models.ts +1 -35
  129. package/src/cli/local-bridge/facade-api.ts +0 -217
  130. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  131. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  132. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  133. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  134. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  135. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  136. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  137. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  138. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  139. package/src/cli/local-bridge/job-store.ts +1 -490
  140. package/src/cli/local-bridge/server.ts +0 -513
  141. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  142. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  143. package/src/cli/mcp/auth.ts +5 -5
  144. package/src/cli/mcp/instructions.ts +3 -2
  145. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  146. package/src/cli/mcp/multi-repository.ts +16 -5
  147. package/src/cli/mcp/oauth.ts +175 -26
  148. package/src/cli/mcp/repository-tools.ts +247 -107
  149. package/src/cli/mcp/server.ts +537 -111
  150. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  151. package/src/cli/mcp/toolset-selection.ts +1 -1
  152. package/src/cli/mcp/toolset.ts +42 -5
  153. package/src/cli/mcp/transports/http.ts +101 -14
  154. package/src/cli/mcp/transports/session-registry.ts +43 -5
  155. package/src/cli/mcp/types.ts +3 -3
  156. package/src/cli/repositories/command-classifier.ts +101 -19
  157. package/src/cli/repositories/command-executor.ts +55 -521
  158. package/src/cli/repositories/command-process.ts +108 -0
  159. package/src/cli/repositories/command-scope.ts +99 -41
  160. package/src/cli/repositories/controller-home.ts +7 -8
  161. package/src/cli/repositories/git-command-executor.ts +2 -0
  162. package/src/cli/repositories/locks.ts +53 -16
  163. package/src/cli/repositories/repository-snapshot.ts +121 -0
  164. package/src/cli/repositories/selected-path-actions.ts +17 -9
  165. package/src/cli/repositories/structured-git.ts +61 -3
  166. package/src/cli/repository/inspector.ts +553 -63
  167. package/src/cli/repository/session-cache.ts +58 -17
  168. package/src/cli/v81-entry.ts +1 -4
  169. package/src/effects/fs-transaction.ts +85 -17
  170. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  171. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  172. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  173. package/src/runtime/context/lsp-client.ts +283 -0
  174. package/src/runtime/context/semantic-navigation.ts +275 -0
  175. package/src/runtime/context/swift-navigation.ts +565 -0
  176. package/src/runtime/context/typescript-navigation.ts +226 -0
  177. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  178. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +47 -8
  179. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  180. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  181. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  182. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  183. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  184. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  185. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  186. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
  187. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  188. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
  189. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  190. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  191. package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
  192. package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
  193. package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
  194. package/src/runtime/control-plane/facade/index.ts +1 -0
  195. package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
  196. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  197. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  198. package/src/runtime/control-plane/facade/types.ts +22 -5
  199. package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
  200. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  201. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  202. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  203. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  204. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  205. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  206. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  207. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  208. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  209. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  210. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  211. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  212. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  213. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  214. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  215. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  216. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  217. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  218. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  219. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
  220. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  221. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  222. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  223. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  224. package/src/runtime/control-plane/release-retention.ts +62 -2
  225. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  226. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  227. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  228. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  229. package/src/runtime/diagnostics/performance.ts +24 -1
  230. package/src/runtime/evidence/event-ledger.ts +1 -1
  231. package/src/runtime/execution/jobs/store.ts +58 -4
  232. package/src/runtime/execution/jobs/types.ts +1 -1
  233. package/src/runtime/execution/managed-workspace.ts +203 -1
  234. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  235. package/src/runtime/execution/process-runtime/check-result.ts +53 -0
  236. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  237. package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
  238. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  239. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  240. package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
  241. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  242. package/src/runtime/execution/process-runtime/runtime.ts +43 -3
  243. package/src/runtime/execution/process-runtime/store.ts +1 -0
  244. package/src/runtime/execution/process-runtime/types.ts +2 -0
  245. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  246. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  247. package/src/runtime/execution/workers/executor.ts +0 -48
  248. package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
  249. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  250. package/src/runtime/gateway/mcp/router.ts +184 -82
  251. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  252. package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
  253. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  254. package/src/runtime/plugins/browser-adapter.ts +2901 -241
  255. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  256. package/src/runtime/plugins/browser-handoff.ts +92 -2
  257. package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
  258. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  259. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  260. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  261. package/src/runtime/plugins/browser-runtime.ts +183 -0
  262. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  263. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  264. package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
  265. package/src/runtime/plugins/external-adapter.ts +324 -3
  266. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  267. package/src/runtime/plugins/first-party-registry.ts +2 -0
  268. package/src/runtime/plugins/github-adapter.ts +125 -0
  269. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  270. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  271. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  272. package/src/runtime/plugins/ios-adapter.ts +106 -16
  273. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  274. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  275. package/src/runtime/plugins/lightweight-action.ts +153 -0
  276. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  277. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  278. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  279. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  280. package/src/runtime/plugins/resend-adapter.ts +1 -1
  281. package/src/runtime/plugins/store.ts +269 -29
  282. package/src/runtime/plugins/types.ts +35 -2
  283. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  284. package/src/runtime/projections/git-status-sampler.ts +16 -16
  285. package/src/runtime/projections/materialized-view.ts +28 -10
  286. package/src/runtime/recovery/maintenance-executor.ts +393 -28
  287. package/src/runtime/root/mcp-transport.ts +180 -10
  288. package/src/runtime/root/package-connector-service.ts +186 -8
  289. package/src/runtime/root/package-runtime-release.ts +143 -17
  290. package/src/runtime/root/package-runtime-service.ts +430 -22
  291. package/src/runtime/root/release-manifest.ts +7 -2
  292. package/src/runtime/root/release-materialize.ts +19 -0
  293. package/src/runtime/root/release-store.ts +48 -11
  294. package/src/runtime/root/runtime.ts +23 -5
  295. package/src/runtime/root/service.ts +100 -29
  296. package/src/runtime/root/types.ts +2 -0
  297. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  298. package/src/runtime/shared/trusted-executable.ts +62 -0
  299. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  300. package/src/runtime/standalone-recovery/core.ts +700 -92
  301. package/src/runtime/standalone-recovery/entry.ts +99 -23
  302. package/src/runtime/standalone-recovery/installer.ts +19 -3
  303. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  304. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  305. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  306. package/src/runtime/workflow/schedules/engine.ts +287 -151
  307. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  308. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  309. package/src/runtime/workflow/schedules/store.ts +131 -10
  310. package/src/runtime/workflow/schedules/types.ts +2 -4
  311. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  312. package/src/testing/test-governance.ts +8 -40
  313. package/assets/reference-configs/heartbeat-triage.md +0 -57
  314. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  315. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  316. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  317. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  318. package/docs/architecture/current/architecture-invariants.md +0 -331
  319. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  320. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  321. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  322. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  323. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  324. package/docs/architecture/current/entity-model.md +0 -612
  325. package/docs/architecture/current/external-plugin-governance.md +0 -396
  326. package/docs/architecture/current/failure-recovery.md +0 -447
  327. package/docs/architecture/current/governance.md +0 -213
  328. package/docs/architecture/current/human-interaction-plane.md +0 -87
  329. package/docs/architecture/current/implementation-status.md +0 -128
  330. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  331. package/docs/architecture/current/legacy-governance.md +0 -58
  332. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  333. package/docs/architecture/current/migration-roadmap.md +0 -105
  334. package/docs/architecture/current/multi-repository-execution.md +0 -371
  335. package/docs/architecture/current/operations-runbook.md +0 -45
  336. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  337. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  338. package/docs/architecture/current/repository-access-modes.md +0 -102
  339. package/docs/architecture/current/route-policy.md +0 -15
  340. package/docs/architecture/current/route-session-concurrency.md +0 -85
  341. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  342. package/docs/architecture/current/runtime-directory-map.md +0 -49
  343. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  344. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  345. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  346. package/docs/architecture/current/system-overview.md +0 -169
  347. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  348. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  349. package/docs/architecture/current/thin-harness-v1.md +0 -321
  350. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  351. package/docs/forge-autonomous-goal-loop.md +0 -9
  352. package/docs/forge-provider-configuration.md +0 -7
  353. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  354. package/docs/operations/gmail-assistant-routines.md +0 -186
  355. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  356. package/docs/operations/provider-configuration.md +0 -31
  357. package/scripts/harness-trace-grade.sh +0 -150
  358. package/scripts/heartbeat-triage.sh +0 -361
  359. package/scripts/maintenance-triage.sh +0 -167
  360. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  361. package/src/cli/repositories/goal-registry.ts +0 -239
  362. package/src/runtime/assistant/action-proposals.ts +0 -279
  363. package/src/runtime/assistant/intent.ts +0 -395
  364. package/src/runtime/assistant/model-provider.ts +0 -460
  365. package/src/runtime/assistant/openapi.ts +0 -223
  366. package/src/runtime/assistant/policy.ts +0 -89
  367. package/src/runtime/assistant/readiness.ts +0 -258
  368. package/src/runtime/assistant/routine-runtime.ts +0 -477
  369. package/src/runtime/assistant/schedule-binding.ts +0 -171
  370. package/src/runtime/assistant/standing-grants.ts +0 -375
  371. package/src/runtime/assistant/store.ts +0 -243
  372. package/src/runtime/assistant/types.ts +0 -135
  373. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  374. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  375. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  376. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  377. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  378. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  379. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  380. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  381. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  382. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  383. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  384. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  385. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  386. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  387. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  388. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  389. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  390. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  391. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  392. package/src/runtime/workflow/findings/store.ts +0 -204
  393. package/src/runtime/workflow/findings/types.ts +0 -58
  394. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  395. package/src/runtime/workflow/portfolio/store.ts +0 -185
  396. 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
+ }
@@ -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