@moretea-labs/forge 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +46 -0
  3. package/CLAUDE.md +2 -0
  4. package/DESIGN.md +7 -2
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +2 -2
  7. package/SKILL.md +2 -2
  8. package/THIRD_PARTY_NOTICES.md +1 -1
  9. package/agents/openai.yaml +1 -1
  10. package/assets/hooks/hook-input.sh +11 -8
  11. package/assets/hooks/lib/session-state.sh +10 -7
  12. package/assets/hooks/lib/workflow-state.sh +13 -177
  13. package/assets/hooks/post-tool-observer.sh +8 -2
  14. package/assets/hooks/pre-edit-guard.sh +8 -39
  15. package/assets/hooks/prompt-guard.sh +7 -83
  16. package/assets/hooks/session-start-context.sh +56 -1
  17. package/assets/partials/05-workflow.partial.md +1 -1
  18. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  19. package/assets/plugin-registry.v1.json +54 -3
  20. package/assets/reference-configs/document-generation.md +1 -1
  21. package/assets/reference-configs/harness-overview.md +1 -1
  22. package/assets/reference-configs/hook-operations.md +1 -1
  23. package/assets/reference-configs/sprint-contracts.md +3 -3
  24. package/assets/skill-version.json +2 -2
  25. package/assets/templates/contract.template.md +2 -7
  26. package/assets/templates/helpers/capture-plan.sh +2 -2
  27. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  28. package/assets/templates/helpers/contract-worktree.sh +1 -21
  29. package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/new-plan.sh +1 -1
  32. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  33. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  34. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/plan.template.md +1 -1
  38. package/assets/templates/review.template.md +9 -34
  39. package/assets/workflow-contract.v1.json +10 -19
  40. package/docs/README.md +10 -10
  41. package/docs/architecture/CURRENT.md +170 -0
  42. package/docs/architecture/EVOLUTION.md +42 -0
  43. package/docs/architecture/current/README.md +4 -156
  44. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  45. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  46. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  47. package/docs/architecture/history/README.md +7 -0
  48. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  49. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  50. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  51. package/docs/architecture/history.md +3 -34
  52. package/docs/architecture/index.md +15 -98
  53. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  54. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  55. package/docs/architecture/versions/1.6.md +38 -0
  56. package/docs/architecture/versions/1.7.md +41 -0
  57. package/docs/forge-chatgpt-browser-engine.md +2 -2
  58. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  59. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  60. package/docs/forge-github-issue-launcher.md +1 -1
  61. package/docs/forge-runtime-self-healing-loop.md +1 -1
  62. package/docs/operations/controller-browser-plugin.md +38 -22
  63. package/docs/operations/controller-performance-and-502.md +124 -0
  64. package/docs/operations/controller-reliability-runbook.md +1 -1
  65. package/docs/operations/features.md +3 -3
  66. package/docs/operations/github-repository.md +1 -1
  67. package/docs/operations/homebrew.md +2 -2
  68. package/docs/operations/mcp-tool-exposure.md +5 -3
  69. package/docs/tutorials/01-install-and-start.md +1 -1
  70. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Installation.md +1 -1
  74. package/docs/wiki/Integrations.md +2 -2
  75. package/docs/wiki/Quick-Start.md +1 -1
  76. package/docs/wiki/Work-Lifecycle.md +1 -1
  77. package/package.json +29 -10
  78. package/scripts/activate-source-baseline.ts +17 -13
  79. package/scripts/benchmark-background-check-overlap.ts +228 -0
  80. package/scripts/benchmark-mcp-host.sh +117 -0
  81. package/scripts/benchmark-mcp-transport.ts +170 -0
  82. package/scripts/benchmark-quality-harness.ts +146 -0
  83. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  84. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  85. package/scripts/benchmark-typescript-navigation.ts +186 -0
  86. package/scripts/benchmark-work-efficiency.ts +334 -0
  87. package/scripts/capture-plan.sh +3 -3
  88. package/scripts/check-agent-tooling.sh +8 -35
  89. package/scripts/check-architecture-sync.sh +17 -86
  90. package/scripts/check-mcp-compatibility.ts +14 -0
  91. package/scripts/check-public-docs.sh +14 -1
  92. package/scripts/check-runtime-architecture.mjs +107 -64
  93. package/scripts/check-stable-baseline.ts +92 -0
  94. package/scripts/check-task-workflow.sh +4 -4
  95. package/scripts/contract-worktree.sh +1 -21
  96. package/scripts/ensure-task-workflow.sh +22 -55
  97. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  98. package/scripts/install-standalone-recovery.ts +24 -0
  99. package/scripts/lib/project-init-lib.sh +15 -44
  100. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  101. package/scripts/migrate-project-template.sh +23 -1179
  102. package/scripts/new-plan.sh +1 -1
  103. package/scripts/plan-to-todo.sh +11 -36
  104. package/scripts/run-governed-gate.ts +2 -1
  105. package/scripts/ship-worktrees.sh +2 -29
  106. package/scripts/smoke-ios-physical-device.ts +4 -2
  107. package/scripts/smoke-runtime-recovery.ts +1 -35
  108. package/scripts/smoke-schedule-engine.ts +1 -20
  109. package/scripts/sprint-backlog.sh +1 -1
  110. package/scripts/verify-contract.sh +1 -111
  111. package/scripts/verify-forge-runtime.sh +0 -1
  112. package/scripts/verify-sprint.sh +3 -180
  113. package/scripts/workflow-contract.ts +1 -5
  114. package/src/cli/commands/chatgpt.ts +31 -2
  115. package/src/cli/commands/init-hook.ts +3 -3
  116. package/src/cli/commands/mcp.ts +9 -5
  117. package/src/cli/commands/plugin.ts +37 -10
  118. package/src/cli/commands/recovery.ts +25 -3
  119. package/src/cli/commands/runtime.ts +55 -3
  120. package/src/cli/commands/security.ts +12 -1
  121. package/src/cli/commands/setup-profile.ts +31 -21
  122. package/src/cli/controller/check-runner.ts +106 -3
  123. package/src/cli/controller/composite-result.ts +1 -1
  124. package/src/cli/controller/context/focus.ts +51 -0
  125. package/src/cli/controller/context/instruction-context.ts +114 -0
  126. package/src/cli/controller/context/known-paths.ts +76 -0
  127. package/src/cli/controller/context/query-planning.ts +110 -0
  128. package/src/cli/controller/context/rule-resolution.ts +532 -0
  129. package/src/cli/controller/context/source-materializer.ts +278 -0
  130. package/src/cli/controller/context/structural.ts +16 -0
  131. package/src/cli/controller/context/types.ts +286 -0
  132. package/src/cli/controller/context-pack.ts +898 -430
  133. package/src/cli/controller/launch-agents.ts +77 -2
  134. package/src/cli/controller/operational-plan.ts +2 -2
  135. package/src/cli/controller/work-mode.ts +6 -4
  136. package/src/cli/editing/edit-session.ts +60 -7
  137. package/src/cli/github/github.ts +133 -0
  138. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  139. package/src/cli/hook/prompt-intents.ts +1 -1
  140. package/src/cli/installer/managed-entries.ts +15 -1
  141. package/src/cli/local-bridge/console-automations.ts +225 -67
  142. package/src/cli/local-bridge/console-view-models.ts +3 -35
  143. package/src/cli/local-bridge/facade-api.ts +2 -217
  144. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  145. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
  146. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  147. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
  148. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  149. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  150. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
  151. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  152. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  153. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
  154. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  155. package/src/cli/local-bridge/frontend/src/types.ts +8 -7
  156. package/src/cli/local-bridge/job-store.ts +1 -490
  157. package/src/cli/local-bridge/server.ts +29 -532
  158. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  159. package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
  160. package/src/cli/mcp/auth.ts +5 -5
  161. package/src/cli/mcp/instructions.ts +3 -2
  162. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  163. package/src/cli/mcp/multi-repository.ts +16 -5
  164. package/src/cli/mcp/oauth.ts +175 -26
  165. package/src/cli/mcp/paths.ts +8 -2
  166. package/src/cli/mcp/repository-tools.ts +356 -101
  167. package/src/cli/mcp/server.ts +625 -54
  168. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  169. package/src/cli/mcp/toolset-selection.ts +1 -1
  170. package/src/cli/mcp/toolset.ts +42 -5
  171. package/src/cli/mcp/transports/http.ts +125 -15
  172. package/src/cli/mcp/transports/session-registry.ts +67 -5
  173. package/src/cli/mcp/types.ts +3 -3
  174. package/src/cli/repositories/command-classifier.ts +209 -14
  175. package/src/cli/repositories/command-executor.ts +55 -521
  176. package/src/cli/repositories/command-process.ts +108 -0
  177. package/src/cli/repositories/command-scope.ts +99 -41
  178. package/src/cli/repositories/controller-home.ts +7 -8
  179. package/src/cli/repositories/git-command-executor.ts +2 -0
  180. package/src/cli/repositories/locks.ts +53 -16
  181. package/src/cli/repositories/repository-snapshot.ts +121 -0
  182. package/src/cli/repositories/safe-patch.ts +6 -4
  183. package/src/cli/repositories/selected-path-actions.ts +17 -9
  184. package/src/cli/repositories/structured-git.ts +110 -10
  185. package/src/cli/repository/inspector.ts +565 -62
  186. package/src/cli/repository/session-cache.ts +58 -17
  187. package/src/cli/v81-entry.ts +1 -4
  188. package/src/effects/fs-transaction.ts +85 -17
  189. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  190. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  191. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  192. package/src/runtime/context/lsp-client.ts +283 -0
  193. package/src/runtime/context/semantic-navigation.ts +275 -0
  194. package/src/runtime/context/swift-navigation.ts +565 -0
  195. package/src/runtime/context/typescript-navigation.ts +226 -0
  196. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  197. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
  198. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  199. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  200. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  201. package/src/runtime/control-plane/execution/validation.ts +15 -2
  202. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  203. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  204. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  205. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  206. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  207. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
  208. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
  209. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
  210. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  211. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  212. package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
  213. package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
  214. package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
  215. package/src/runtime/control-plane/facade/index.ts +1 -0
  216. package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
  217. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  218. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  219. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  220. package/src/runtime/control-plane/facade/types.ts +55 -11
  221. package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
  222. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  223. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  224. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  225. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  226. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  227. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  228. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  229. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  230. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  231. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  232. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  233. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  234. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  235. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  236. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  237. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  238. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  239. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  240. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  241. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
  242. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  243. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  244. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  245. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  246. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  247. package/src/runtime/control-plane/release-retention.ts +62 -2
  248. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  249. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  250. package/src/runtime/diagnostics/mcp-timing.ts +19 -0
  251. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  252. package/src/runtime/diagnostics/performance.ts +24 -1
  253. package/src/runtime/evidence/event-ledger.ts +1 -1
  254. package/src/runtime/execution/jobs/store.ts +58 -4
  255. package/src/runtime/execution/jobs/types.ts +1 -1
  256. package/src/runtime/execution/managed-workspace.ts +203 -1
  257. package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
  258. package/src/runtime/execution/process-runtime/check-result.ts +63 -2
  259. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  260. package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
  261. package/src/runtime/execution/process-runtime/gc.ts +98 -12
  262. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  263. package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
  264. package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
  265. package/src/runtime/execution/process-runtime/runtime.ts +59 -4
  266. package/src/runtime/execution/process-runtime/store.ts +7 -0
  267. package/src/runtime/execution/process-runtime/types.ts +2 -0
  268. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  269. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  270. package/src/runtime/execution/workers/executor.ts +0 -48
  271. package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
  272. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  273. package/src/runtime/gateway/mcp/router.ts +185 -83
  274. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  275. package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
  276. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  277. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  278. package/src/runtime/plugins/browser-adapter.ts +2925 -318
  279. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  280. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  281. package/src/runtime/plugins/browser-handoff.ts +92 -5
  282. package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
  283. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  284. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  285. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  286. package/src/runtime/plugins/browser-runtime.ts +183 -0
  287. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  288. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  289. package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
  290. package/src/runtime/plugins/external-adapter.ts +324 -3
  291. package/src/runtime/plugins/external-registration.ts +84 -5
  292. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  293. package/src/runtime/plugins/first-party-registry.ts +4 -2
  294. package/src/runtime/plugins/github-adapter.ts +125 -0
  295. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  296. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  297. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  298. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  299. package/src/runtime/plugins/ios-adapter.ts +106 -16
  300. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  301. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  302. package/src/runtime/plugins/lightweight-action.ts +153 -0
  303. package/src/runtime/plugins/local-system-adapter.ts +125 -8
  304. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  305. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  306. package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
  307. package/src/runtime/plugins/resend-adapter.ts +1 -1
  308. package/src/runtime/plugins/store.ts +366 -24
  309. package/src/runtime/plugins/types.ts +39 -2
  310. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  311. package/src/runtime/projections/git-status-sampler.ts +16 -16
  312. package/src/runtime/projections/materialized-view.ts +28 -10
  313. package/src/runtime/recovery/actions.ts +0 -10
  314. package/src/runtime/recovery/classifier.ts +0 -9
  315. package/src/runtime/recovery/maintenance-executor.ts +415 -18
  316. package/src/runtime/recovery/probe.ts +0 -1
  317. package/src/runtime/recovery/types.ts +0 -1
  318. package/src/runtime/root/mcp-transport.ts +180 -10
  319. package/src/runtime/root/package-connector-service.ts +186 -8
  320. package/src/runtime/root/package-runtime-release.ts +143 -17
  321. package/src/runtime/root/package-runtime-service.ts +430 -22
  322. package/src/runtime/root/release-manifest.ts +7 -2
  323. package/src/runtime/root/release-materialize.ts +162 -19
  324. package/src/runtime/root/release-store.ts +48 -11
  325. package/src/runtime/root/runtime.ts +23 -5
  326. package/src/runtime/root/service.ts +100 -27
  327. package/src/runtime/root/types.ts +2 -0
  328. package/src/runtime/safe-tooling/index.ts +0 -1
  329. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  330. package/src/runtime/safe-tooling/types.ts +0 -30
  331. package/src/runtime/shared/trusted-executable.ts +62 -0
  332. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  333. package/src/runtime/standalone-recovery/core.ts +702 -94
  334. package/src/runtime/standalone-recovery/entry.ts +99 -23
  335. package/src/runtime/standalone-recovery/installer.ts +19 -3
  336. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  337. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  338. package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
  339. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  340. package/src/runtime/workflow/schedules/engine.ts +509 -80
  341. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  342. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  343. package/src/runtime/workflow/schedules/store.ts +131 -10
  344. package/src/runtime/workflow/schedules/types.ts +6 -3
  345. package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
  346. package/src/testing/test-governance.ts +8 -40
  347. package/assets/reference-configs/heartbeat-triage.md +0 -57
  348. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  349. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  350. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  351. package/bin/forge-desktop-helper.mjs +0 -204
  352. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  353. package/docs/architecture/current/architecture-invariants.md +0 -331
  354. package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
  355. package/docs/architecture/current/codegraph-context-provider.md +0 -226
  356. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  357. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  358. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
  359. package/docs/architecture/current/entity-model.md +0 -592
  360. package/docs/architecture/current/external-plugin-governance.md +0 -396
  361. package/docs/architecture/current/failure-recovery.md +0 -447
  362. package/docs/architecture/current/governance.md +0 -213
  363. package/docs/architecture/current/human-interaction-plane.md +0 -87
  364. package/docs/architecture/current/implementation-status.md +0 -128
  365. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  366. package/docs/architecture/current/legacy-governance.md +0 -58
  367. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  368. package/docs/architecture/current/migration-roadmap.md +0 -105
  369. package/docs/architecture/current/multi-repository-execution.md +0 -371
  370. package/docs/architecture/current/operations-runbook.md +0 -45
  371. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  372. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  373. package/docs/architecture/current/repository-access-modes.md +0 -102
  374. package/docs/architecture/current/route-policy.md +0 -15
  375. package/docs/architecture/current/route-session-concurrency.md +0 -85
  376. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  377. package/docs/architecture/current/runtime-directory-map.md +0 -49
  378. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  379. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  380. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  381. package/docs/architecture/current/system-overview.md +0 -144
  382. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  383. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  384. package/docs/architecture/current/thin-harness-v1.md +0 -311
  385. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  386. package/docs/forge-autonomous-goal-loop.md +0 -9
  387. package/docs/forge-provider-configuration.md +0 -7
  388. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
  389. package/docs/operations/controller-desktop-plugin.md +0 -112
  390. package/docs/operations/gmail-assistant-routines.md +0 -186
  391. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  392. package/docs/operations/provider-configuration.md +0 -31
  393. package/scripts/harness-trace-grade.sh +0 -150
  394. package/scripts/heartbeat-triage.sh +0 -361
  395. package/scripts/maintenance-triage.sh +0 -167
  396. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  397. package/src/cli/repositories/goal-registry.ts +0 -239
  398. package/src/runtime/assistant/action-proposals.ts +0 -279
  399. package/src/runtime/assistant/intent.ts +0 -395
  400. package/src/runtime/assistant/model-provider.ts +0 -460
  401. package/src/runtime/assistant/openapi.ts +0 -223
  402. package/src/runtime/assistant/policy.ts +0 -89
  403. package/src/runtime/assistant/readiness.ts +0 -258
  404. package/src/runtime/assistant/routine-runtime.ts +0 -477
  405. package/src/runtime/assistant/schedule-binding.ts +0 -171
  406. package/src/runtime/assistant/standing-grants.ts +0 -375
  407. package/src/runtime/assistant/store.ts +0 -243
  408. package/src/runtime/assistant/types.ts +0 -135
  409. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  410. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  411. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  412. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  413. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  414. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  415. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  416. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  417. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  418. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  419. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  420. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  421. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  422. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  423. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  424. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  425. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  426. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  427. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  428. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  429. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
  430. package/src/runtime/workflow/findings/store.ts +0 -204
  431. package/src/runtime/workflow/findings/types.ts +0 -58
  432. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  433. package/src/runtime/workflow/portfolio/store.ts +0 -185
  434. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -9,10 +9,15 @@ import {
9
9
  previewAutomaticRuntimeMaintenance,
10
10
  type RuntimeMaintenanceActionId,
11
11
  } from '../../recovery/maintenance-executor';
12
- import { listCandidateFindings } from '../findings/store';
13
- import { getControllerSession, getWorkContract, isTerminalWorkContractStatus, listHandoffItems } from '../../control-plane/facade';
12
+ import { appendWorkEvidence, controllerSessionBlocksRecovery, getControllerSession, getWorkContract, isTerminalWorkContractStatus, listHandoffItems } from '../../control-plane/facade';
14
13
  import { launchSuperController } from '../../control-plane/launcher/thin-launcher';
15
14
  import { getExternalControllerLaunchReservation } from '../../control-plane/launcher/launch-reservation-store';
15
+ import { runStandaloneChatgptPrompt, runWorkChatgptContinuation } from '../../control-plane/launcher/chatgpt-work-continuation';
16
+ import {
17
+ beginInitialControllerRoundDispatch,
18
+ buildControllerRoundRelayPrompt,
19
+ finishControllerRoundRelayDispatch,
20
+ } from '../../control-plane/facade/controller-round-relay';
16
21
  import {
17
22
  getOccurrence,
18
23
  getSchedule,
@@ -24,7 +29,9 @@ import {
24
29
  saveSchedule,
25
30
  saveScheduleDecision,
26
31
  } from './store';
27
- import { applyScheduleFailure } from './settlement';
32
+ import { applyScheduleFailure, applyScheduleRetryableFailure } from './settlement';
33
+ import { classifyScheduledBrowserObservation, executeScheduledBrowserProbe } from './browser-probe';
34
+ import { executeScheduledGithubIssueWatch } from './github-issue-watch';
28
35
  import type {
29
36
  RepositorySchedule,
30
37
  ScheduleDecisionType,
@@ -35,12 +42,24 @@ import type {
35
42
  const execFileAsync = promisify(execFile);
36
43
  const LIVE_MAINTENANCE_OPERATION = 'runtime_maintenance_apply';
37
44
  const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
45
+ const BROWSER_PROBE_OPERATION = 'browser_probe';
46
+ const GITHUB_ISSUE_WATCH_OPERATION = 'github_issue_watch';
38
47
 
39
48
  function isDeterministicSchedule(schedule: RepositorySchedule): boolean {
40
- // Kernel may execute only fully deterministic, allowlisted schedule operations.
41
- // Semantic / model-backed schedules remain external-controller handoffs.
49
+ // Kernel executes only typed, bounded dispatch operations.
42
50
  return schedule.action.operation === LIVE_MAINTENANCE_OPERATION
43
- || schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION;
51
+ || schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
52
+ || schedule.action.operation === BROWSER_PROBE_OPERATION
53
+ || schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION;
54
+ }
55
+
56
+ function retiredScheduleReason(schedule: RepositorySchedule): string | undefined {
57
+ if (schedule.action.operation === 'chatgpt_browser_prompt') return 'RETIRED_WORK_FREE_PROMPT_SCHEDULE';
58
+ if (schedule.action.operation === 'assistant_routine_execute') return 'RETIRED_PERSONAL_ASSISTANT_SCHEDULE';
59
+ if ((schedule.trigger.condition as { kind?: string } | undefined)?.kind === 'candidate_observation_threshold') {
60
+ return 'RETIRED_CANDIDATE_FINDING_SCHEDULE';
61
+ }
62
+ return undefined;
44
63
  }
45
64
 
46
65
  function normalizedWindow(minutes: number, at = Date.now()): string {
@@ -101,6 +120,12 @@ function cronWindowKey(schedule: RepositorySchedule, at = Date.now()): string {
101
120
  }
102
121
 
103
122
  function triggerWindowKey(schedule: RepositorySchedule, context: ScheduleTriggerContext | undefined, at = Date.now()): string {
123
+ // Explicit control-plane triggers use a separate identity namespace from timer/cron windows.
124
+ // A caller-supplied event/request id is the stable retry key; otherwise each manual trigger is distinct.
125
+ if (context?.source === 'manual' && schedule.trigger.type !== 'manual') {
126
+ const stableManualKey = context.eventId?.trim();
127
+ return `manual:${stableManualKey || randomUUID()}`;
128
+ }
104
129
  switch (schedule.trigger.type) {
105
130
  case 'cron':
106
131
  return cronWindowKey(schedule, at);
@@ -223,19 +248,15 @@ async function triggerDue(
223
248
  const due = condition.kind === 'job_succeeded' ? job?.status === 'succeeded' : terminal;
224
249
  return { due, reason: due ? undefined : `Condition ${condition.kind} is not met.`, evidence: { jobId: condition.jobId, status: job?.status ?? 'missing' } };
225
250
  }
226
- const threshold = Math.max(1, condition.observationThreshold ?? 2);
227
- const finding = listCandidateFindings(controllerHome, schedule.repoId, { includeTerminal: true, limit: 1000 })
228
- .find((entry) => entry.semanticKey === condition.semanticKey);
229
- const due = Boolean(finding && finding.observationCount >= threshold);
230
- return { due, reason: due ? undefined : 'Candidate observation threshold is not met.', evidence: { semanticKey: condition.semanticKey, observations: finding?.observationCount ?? 0, threshold } };
231
251
  }
232
252
  default:
233
253
  return { due: false, reason: 'Unsupported Schedule trigger.' };
234
254
  }
235
255
  }
236
256
 
237
- function continuationWorkId(schedule: RepositorySchedule): string | undefined {
238
- if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION) return undefined;
257
+ function workBoundScheduleWorkId(schedule: RepositorySchedule): string | undefined {
258
+ if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
259
+ && schedule.action.operation !== BROWSER_PROBE_OPERATION) return undefined;
239
260
  const value = schedule.action.arguments?.work_id;
240
261
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
241
262
  }
@@ -251,7 +272,7 @@ function executionJobBelongsToWork(job: ReturnType<typeof listExecutionJobs>[num
251
272
 
252
273
  async function stopReason(controllerHome: string, schedule: RepositorySchedule): Promise<string | undefined> {
253
274
  const projection = readRepositoryProjection(controllerHome, schedule.repoId);
254
- const workId = continuationWorkId(schedule);
275
+ const workId = workBoundScheduleWorkId(schedule);
255
276
  const work = workId ? getWorkContract({ controllerHome, repoId: schedule.repoId }, workId) : undefined;
256
277
  if (schedule.stopConditions.includes('human_review_required')) {
257
278
  if (workId) {
@@ -344,12 +365,284 @@ function decideOccurrence(
344
365
  return saveOccurrence(controllerHome, { ...occurrence, decision, decisionId, status, reason });
345
366
  }
346
367
 
368
+ export type ChatgptWakeFailureClass = 'retryable_readiness' | 'semantic_wait' | 'user_action_required' | 'ordinary_failure';
369
+
370
+ export function classifyChatgptWakeFailure(reason: string): ChatgptWakeFailureClass {
371
+ const normalized = reason.toUpperCase();
372
+ if (
373
+ normalized.includes('CHATGPT_AUTOMATION_LOGIN_REQUIRED')
374
+ || normalized.includes('PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED')
375
+ || normalized.includes('AUTHENTICATION_REQUIRED')
376
+ || normalized.includes('CONSENT_REQUIRED')
377
+ ) return 'user_action_required';
378
+ if (normalized.includes('REPEATED_STATE:')) return 'semantic_wait';
379
+ const retryableMarkers = [
380
+ 'PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
381
+ 'PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE',
382
+ 'PLUGIN_BROWSER_NODE_HOST_UNAVAILABLE',
383
+ 'PLUGIN_BROWSER_NODE_UNAVAILABLE',
384
+ 'PLUGIN_BROWSER_ACTIVE_TAB_UNAVAILABLE',
385
+ 'PLUGIN_BROWSER_SESSION_STATE_LOST',
386
+ 'PLUGIN_SESSION_NOT_FOUND',
387
+ 'PLUGIN_BROWSER_NATIVE_FOREGROUND_ACTIVATOR_UNAVAILABLE',
388
+ 'CHATGPT_CONTROLLER_BROWSER_ROOT_UNAVAILABLE',
389
+ 'CHATGPT_CONTROLLER_BROWSER_FAILED',
390
+ 'CHATGPT_AUTOMATION_COMPOSER_UNAVAILABLE',
391
+ 'CHATGPT_AUTOMATION_INTELLIGENCE_CONTROL_UNAVAILABLE',
392
+ 'CHATGPT_AUTOMATION_REPLACEMENT_SESSION_NOT_CONFIRMED',
393
+ 'HTTP 502',
394
+ 'ECONNREFUSED',
395
+ 'ECONNRESET',
396
+ 'ETIMEDOUT',
397
+ 'UND_ERR_CONNECT_TIMEOUT',
398
+ ];
399
+ if (retryableMarkers.some((marker) => normalized.includes(marker))) return 'retryable_readiness';
400
+ return 'ordinary_failure';
401
+ }
402
+
403
+ function externalControllerLaunchArgs(args: Record<string, unknown>, controllerType: string): string[] {
404
+ const launchArgs = Array.isArray(args.launch_args) ? args.launch_args.map(String) : [];
405
+ if (controllerType !== 'chatgpt') return launchArgs;
406
+ const explicit = new Set(launchArgs.filter((value) => value.startsWith('--')).map((value) => value.split('=', 1)[0]));
407
+ const add = (flag: string, value: unknown) => {
408
+ if (explicit.has(flag) || typeof value !== 'string' || !value.trim()) return;
409
+ launchArgs.push(flag, value.trim());
410
+ };
411
+ add('--model', args.model);
412
+ add('--reasoning', args.reasoning);
413
+ add('--tab-policy', args.tab_policy);
414
+ return launchArgs;
415
+ }
416
+
417
+ async function executeExternalControllerWake(
418
+ controllerHome: string,
419
+ schedule: RepositorySchedule,
420
+ occurrence: ScheduleOccurrence,
421
+ timestamp: string,
422
+ args: Record<string, unknown>,
423
+ extraEvidence: Record<string, unknown> = {},
424
+ options: { transientWakeFailure?: boolean } = {},
425
+ ): Promise<ScheduleOccurrence> {
426
+ const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
427
+ const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
428
+ if (!workId) {
429
+ return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_WORK_ID_REQUIRED');
430
+ }
431
+ if (!['chatgpt', 'codex', 'claude', 'grok'].includes(controllerType)) {
432
+ return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_TYPE_INVALID');
433
+ }
434
+ const workStore = { controllerHome, repoId: schedule.repoId };
435
+ const work = getWorkContract(workStore, workId);
436
+ if (!work) {
437
+ saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
438
+ return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `EXTERNAL_CONTROLLER_WAKE_WORK_NOT_FOUND:${workId}`);
439
+ }
440
+ if (isTerminalWorkContractStatus(work.status)) {
441
+ saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
442
+ return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); automatic continuation stopped.`);
443
+ }
444
+ const existingOwner = getControllerSession(workStore, workId);
445
+ const occurrenceNowMs = Date.parse(timestamp);
446
+ if (existingOwner && controllerSessionBlocksRecovery(workStore, workId, {
447
+ nowMs: Number.isFinite(occurrenceNowMs) ? occurrenceNowMs : Date.now(),
448
+ })) {
449
+ saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
450
+ return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has an active Controller ${existingOwner.controllerId}.`);
451
+ }
452
+ const launchReservation = getExternalControllerLaunchReservation(workStore, workId);
453
+ if (launchReservation) {
454
+ saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
455
+ return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has a pending external Controller launch ${launchReservation.reservationId}.`);
456
+ }
457
+ const wakeDecision = decideOccurrence(
458
+ controllerHome,
459
+ schedule,
460
+ occurrence,
461
+ 'execute',
462
+ 'running',
463
+ 'Deterministic external Controller wake is starting through Thin Launcher.',
464
+ occurrenceDecisionEvidence({
465
+ operation: schedule.action.operation,
466
+ workId,
467
+ controllerType,
468
+ ...(controllerType === 'chatgpt' ? {
469
+ model: typeof args.model === 'string' ? args.model : 'gpt-5.6',
470
+ reasoning: typeof args.reasoning === 'string' ? args.reasoning : 'high',
471
+ tabPolicy: typeof args.tab_policy === 'string' ? args.tab_policy : 'auto',
472
+ } : {}),
473
+ ...extraEvidence,
474
+ }),
475
+ );
476
+ try {
477
+ const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
478
+ const continuationPrompt = typeof args.continuation_prompt === 'string'
479
+ ? args.continuation_prompt
480
+ : `Scheduled continuation ${occurrence.occurrenceId} from ${schedule.scheduleId}. Read current Forge state and continue only the bounded Work objective.`;
481
+ if (controllerType === 'chatgpt') {
482
+ const reasoning = args.reasoning === 'medium' || args.reasoning === 'xhigh' ? args.reasoning : 'high';
483
+ const tabPolicy = args.tab_policy === 'reuse' || args.tab_policy === 'new' ? args.tab_policy : 'auto';
484
+ let relay;
485
+ try {
486
+ relay = beginInitialControllerRoundDispatch(workStore, {
487
+ workId,
488
+ identity: {
489
+ controllerId: `schedule:${schedule.scheduleId}`,
490
+ principalId: 'forge-scheduler',
491
+ controllerInstanceId: 'forge-runtime-scheduler',
492
+ sessionId: occurrence.occurrenceId,
493
+ },
494
+ requirementId: work.requirementId,
495
+ browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
496
+ conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
497
+ });
498
+ if (relay.status === 'blocked') throw new Error(`CONTROLLER_RELAY_LAUNCH_BLOCKED: ${relay.blockedReason ?? relay.relayScopeId}`);
499
+ const relayPrompt = `${buildControllerRoundRelayPrompt(workStore, relay)}\n\nScheduled continuation hint: ${continuationPrompt}`;
500
+ const dispatched = await runWorkChatgptContinuation({
501
+ controllerHome,
502
+ repoId: schedule.repoId,
503
+ repoRoot: repository.canonicalRoot,
504
+ workId,
505
+ prompt: relayPrompt,
506
+ title: schedule.name,
507
+ browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
508
+ conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
509
+ model: typeof args.model === 'string' ? args.model : 'gpt-5.6',
510
+ reasoning,
511
+ tabPolicy,
512
+ timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
513
+ });
514
+ if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_WORK_CONTINUATION_FAILED');
515
+ const completedRelay = finishControllerRoundRelayDispatch(workStore, {
516
+ workId,
517
+ ok: true,
518
+ browserSessionId: dispatched.browserSessionId,
519
+ conversationUrl: dispatched.conversationUrl,
520
+ });
521
+ const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
522
+ saveSchedule(controllerHome, {
523
+ ...latest,
524
+ lastTriggeredAt: timestamp,
525
+ lastOccurrenceId: occurrence.occurrenceId,
526
+ consecutiveFailures: 0,
527
+ nextEligibleAt: undefined,
528
+ pausedReason: undefined,
529
+ });
530
+ const succeededOccurrence = saveOccurrence(controllerHome, {
531
+ ...wakeDecision,
532
+ status: 'succeeded',
533
+ reason: `ChatGPT dispatch action succeeded via ${dispatched.browserSessionId}; relay ${completedRelay?.relayScopeId ?? relay.relayScopeId} remains dispatched until the new Controller claims Work ${workId}.`,
534
+ });
535
+ appendWorkEvidence(workStore, workId, {
536
+ evidenceId: succeededOccurrence.occurrenceId,
537
+ title: 'scheduled ChatGPT continuation dispatched',
538
+ summary: `Schedule ${schedule.scheduleId} timer occurrence ${succeededOccurrence.occurrenceId} dispatched the Work-bound ChatGPT continuation successfully.`,
539
+ detailLevel: 'summary',
540
+ });
541
+ return succeededOccurrence;
542
+ } catch (error) {
543
+ if (relay) {
544
+ finishControllerRoundRelayDispatch(workStore, {
545
+ workId,
546
+ ok: false,
547
+ error: error instanceof Error ? error.message : String(error),
548
+ });
549
+ }
550
+ if (error instanceof Error && error.message.startsWith('CONTROLLER_RELAY_ROUND_ALREADY_OPEN:')) {
551
+ const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
552
+ saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
553
+ return decideOccurrence(controllerHome, schedule, wakeDecision, 'nothing_to_do', 'skipped', `Work ${workId} already has an open ChatGPT controller round awaiting claim or disposition.`);
554
+ }
555
+ throw error;
556
+ }
557
+ }
558
+ const launched = await launchSuperController({
559
+ work: { controllerHome, repoId: schedule.repoId },
560
+ handoff: { controllerHome, repoId: schedule.repoId },
561
+ }, {
562
+ controllerType: controllerType as 'codex' | 'claude' | 'grok',
563
+ executable: typeof args.executable === 'string' ? args.executable : undefined,
564
+ args: externalControllerLaunchArgs(args, controllerType),
565
+ workId,
566
+ handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
567
+ browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
568
+ conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
569
+ launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
570
+ continuationPrompt,
571
+ cwd: repository.canonicalRoot,
572
+ });
573
+ const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
574
+ saveSchedule(controllerHome, {
575
+ ...latest,
576
+ lastTriggeredAt: timestamp,
577
+ lastOccurrenceId: occurrence.occurrenceId,
578
+ consecutiveFailures: 0,
579
+ nextEligibleAt: undefined,
580
+ pausedReason: undefined,
581
+ });
582
+ return saveOccurrence(controllerHome, {
583
+ ...wakeDecision,
584
+ status: 'succeeded',
585
+ reason: `External Controller wake started ${launched.controllerType} pid=${String(launched.pid ?? 'unknown')} for Work ${workId}.`,
586
+ });
587
+ } catch (error) {
588
+ const reason = error instanceof Error ? error.message : String(error);
589
+ const failureClass = controllerType === 'chatgpt' ? classifyChatgptWakeFailure(reason) : 'ordinary_failure';
590
+ if (options.transientWakeFailure || failureClass === 'retryable_readiness' || failureClass === 'semantic_wait') {
591
+ const semanticWait = failureClass === 'semantic_wait';
592
+ const deferred = applyScheduleRetryableFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
593
+ outcome: semanticWait ? 'skipped' : 'failed',
594
+ decision: semanticWait ? 'nothing_to_do' : 'execute',
595
+ reason: semanticWait
596
+ ? `Controller wake deferred until durable state changes; schedule remains active: ${reason}`
597
+ : `Controller wake deferred by transient readiness; schedule remains active with bounded backoff: ${reason}`,
598
+ countFailure: !semanticWait,
599
+ });
600
+ const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
601
+ saveSchedule(controllerHome, {
602
+ ...latest,
603
+ lastTriggeredAt: timestamp,
604
+ lastOccurrenceId: occurrence.occurrenceId,
605
+ });
606
+ return deferred.occurrence ?? saveOccurrence(controllerHome, {
607
+ ...wakeDecision,
608
+ status: semanticWait ? 'skipped' : 'failed',
609
+ decision: semanticWait ? 'nothing_to_do' : 'execute',
610
+ reason,
611
+ });
612
+ }
613
+ const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
614
+ outcome: 'failed',
615
+ decision: 'execute',
616
+ reason,
617
+ ...(failureClass === 'user_action_required' ? { pauseReason: reason } : {}),
618
+ handoff: {
619
+ title: `External Controller wake ${occurrence.occurrenceId} failed`,
620
+ summary: 'Forge recorded the schedule trigger but could not wake the configured external Controller.',
621
+ reason,
622
+ creationReason: 'repeated_infrastructure_failure',
623
+ blockingDecision: 'Repair the external Controller/browser launch path or update the saved session reference.',
624
+ recommendedDecision: 'Inspect launcher/browser readiness, then retrigger this bounded continuation.',
625
+ recommendedPrompt: `Resume Work ${workId} manually, inspect failed wake occurrence ${occurrence.occurrenceId}, and repair the configured external Controller launch path before unattended continuation resumes.`,
626
+ statusSummary: 'Scheduled external Controller wake failed.',
627
+ blockedBy: ['external_controller_wake_failed'],
628
+ attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, `controller:${controllerType}`],
629
+ },
630
+ });
631
+ const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
632
+ saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
633
+ return failed.occurrence ?? saveOccurrence(controllerHome, { ...wakeDecision, status: 'failed', reason });
634
+ }
635
+ }
636
+
347
637
  export async function evaluateSchedule(
348
638
  controllerHome: string,
349
639
  schedule: RepositorySchedule,
350
640
  force = false,
351
641
  triggerContext?: ScheduleTriggerContext,
352
642
  ): Promise<ScheduleOccurrence | undefined> {
643
+ // Preserve retired schedule records as evidence, but do not create a new
644
+ // occurrence or resume a controller/external action from their old payload.
645
+ if (retiredScheduleReason(schedule)) return undefined;
353
646
  if (!schedule.enabled && !force) return undefined;
354
647
  const due = await triggerDue(controllerHome, schedule, force, triggerContext);
355
648
  if (!due.due) return undefined;
@@ -548,76 +841,212 @@ export async function evaluateSchedule(
548
841
 
549
842
  const args = schedule.action.arguments ?? {};
550
843
  if (schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION) {
551
- const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
552
- const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
553
- if (!workId) {
554
- return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_WORK_ID_REQUIRED');
555
- }
556
- if (!['chatgpt', 'codex', 'claude', 'grok'].includes(controllerType)) {
557
- return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_TYPE_INVALID');
558
- }
559
- const workStore = { controllerHome, repoId: schedule.repoId };
560
- const work = getWorkContract(workStore, workId);
561
- if (!work) {
562
- saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
563
- return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `EXTERNAL_CONTROLLER_WAKE_WORK_NOT_FOUND:${workId}`);
564
- }
565
- if (isTerminalWorkContractStatus(work.status)) {
566
- saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
567
- return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); automatic continuation stopped.`);
844
+ return executeExternalControllerWake(controllerHome, schedule, occurrence, timestamp, args);
845
+ }
846
+
847
+ if (schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION) {
848
+ try {
849
+ const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
850
+ const observation = await executeScheduledGithubIssueWatch({ repoRoot: repository.canonicalRoot, args, observedAt: timestamp });
851
+ const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
852
+ const evidence = occurrenceDecisionEvidence({
853
+ githubRepository: typeof args.github_repository === 'string' ? args.github_repository : undefined,
854
+ observationStatus: observation.status,
855
+ observedIssueCount: observation.issues.length,
856
+ changedIssueNumbers: observation.changedOpenIssues.map((issue) => issue.number),
857
+ });
858
+ const persistObservation = () => saveSchedule(controllerHome, {
859
+ ...getSchedule(controllerHome, schedule.repoId, schedule.scheduleId),
860
+ action: {
861
+ ...latest.action,
862
+ arguments: { ...(latest.action.arguments ?? {}), issue_watch_state: observation.nextState, issue_watch_since: undefined },
863
+ },
864
+ lastTriggeredAt: timestamp,
865
+ lastOccurrenceId: occurrenceId,
866
+ lastObservationAt: timestamp,
867
+ lastObservationStatus: observation.status,
868
+ lastObservationChangedAt: observation.shouldWake ? timestamp : latest.lastObservationChangedAt,
869
+ consecutiveFailures: 0,
870
+ consecutiveNoops: observation.shouldWake ? 0 : (latest.consecutiveNoops ?? 0) + 1,
871
+ nextEligibleAt: undefined,
872
+ pausedReason: undefined,
873
+ });
874
+ if (!observation.shouldWake) {
875
+ const observedSchedule = persistObservation();
876
+ const reason = observation.status === 'baseline'
877
+ ? 'GitHub issue watcher baseline recorded; no Controller wake was emitted.'
878
+ : 'GitHub issue watcher found no new, reopened, or updated open issue.';
879
+ return decideOccurrence(controllerHome, observedSchedule, occurrence, 'nothing_to_do', 'skipped', reason, evidence);
880
+ }
881
+ const issueSummary = observation.changedOpenIssues.slice(0, 10)
882
+ .map((issue) => `#${issue.number} ${issue.title}`)
883
+ .join('; ');
884
+ const wakeArgs = {
885
+ ...args,
886
+ continuation_prompt: typeof args.continuation_prompt === 'string'
887
+ ? `${args.continuation_prompt}\n\nGitHub issue watcher detected: ${issueSummary}`
888
+ : `GitHub issue watcher detected actionable issue changes: ${issueSummary}. Read current Work/Plan/evidence and process these issues through the bounded repair workflow.`,
889
+ };
890
+ const wake = await executeExternalControllerWake(controllerHome, schedule, occurrence, timestamp, wakeArgs, evidence, { transientWakeFailure: true });
891
+ if (wake.status !== 'failed') persistObservation();
892
+ return wake;
893
+ } catch (error) {
894
+ const reason = error instanceof Error ? error.message : String(error);
895
+ const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
896
+ outcome: 'failed',
897
+ decision: 'execute',
898
+ reason,
899
+ handoff: {
900
+ title: `GitHub issue watcher occurrence ${occurrence.occurrenceId} failed`,
901
+ summary: 'The local GitHub issue poll failed before Forge could compare issue state.',
902
+ reason,
903
+ creationReason: 'repeated_infrastructure_failure',
904
+ blockingDecision: 'Repair GitHub CLI authentication/connectivity before unattended issue monitoring resumes.',
905
+ recommendedDecision: 'Restore gh read access, then retrigger the watcher.',
906
+ recommendedPrompt: `Inspect GitHub issue watcher occurrence ${occurrence.occurrenceId} and restore the local read-only gh issue polling path.`,
907
+ statusSummary: 'GitHub issue watcher poll failed.',
908
+ blockedBy: ['github_issue_watch_failed'],
909
+ attemptedActions: [`schedule:${schedule.scheduleId}`, 'gh:api:issues'],
910
+ },
911
+ });
912
+ return failed.occurrence ?? occurrence;
568
913
  }
569
- const existingOwner = getControllerSession(workStore, workId);
570
- if (existingOwner) {
571
- saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
572
- return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has an active Controller ${existingOwner.controllerId}.`);
914
+ }
915
+
916
+ if (schedule.action.operation === BROWSER_PROBE_OPERATION) {
917
+ const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
918
+ const keepaliveOnly = args.keepalive_only === true;
919
+ if (!workId && !keepaliveOnly) {
920
+ return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'SCHEDULE_BROWSER_PROBE_WORK_ID_REQUIRED');
573
921
  }
574
- const launchReservation = getExternalControllerLaunchReservation(workStore, workId);
575
- if (launchReservation) {
576
- saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
577
- return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has a pending external Controller launch ${launchReservation.reservationId}.`);
922
+ if (workId) {
923
+ const workStore = { controllerHome, repoId: schedule.repoId };
924
+ const work = getWorkContract(workStore, workId);
925
+ if (!work) {
926
+ saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
927
+ return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `SCHEDULE_BROWSER_PROBE_WORK_NOT_FOUND:${workId}`);
928
+ }
929
+ if (isTerminalWorkContractStatus(work.status)) {
930
+ saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
931
+ return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); browser watcher stopped before probing.`);
932
+ }
578
933
  }
579
- const wakeDecision = decideOccurrence(
580
- controllerHome,
581
- schedule,
582
- occurrence,
583
- 'execute',
584
- 'running',
585
- 'Deterministic external Controller wake is starting through Thin Launcher.',
586
- occurrenceDecisionEvidence({ operation: schedule.action.operation, workId, controllerType }),
587
- );
934
+
588
935
  try {
589
936
  const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
590
- const launched = launchSuperController({
591
- work: { controllerHome, repoId: schedule.repoId },
592
- handoff: { controllerHome, repoId: schedule.repoId },
593
- }, {
594
- controllerType: controllerType as 'chatgpt' | 'codex' | 'claude' | 'grok',
595
- executable: typeof args.executable === 'string' ? args.executable : undefined,
596
- args: Array.isArray(args.launch_args) ? args.launch_args.map(String) : [],
597
- workId,
598
- handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
599
- browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
600
- conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
601
- launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
602
- continuationPrompt: typeof args.continuation_prompt === 'string'
603
- ? args.continuation_prompt
604
- : `Scheduled continuation ${occurrence.occurrenceId} from ${schedule.scheduleId}. Read current Forge state and continue only the bounded Work objective.`,
605
- cwd: repository.canonicalRoot,
606
- });
937
+ const probe = await executeScheduledBrowserProbe({ controllerHome, repository, occurrenceId, args });
607
938
  const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
608
- saveSchedule(controllerHome, {
939
+ const observationEvidence = occurrenceDecisionEvidence({
940
+ probeStatus: probe.status,
941
+ url: probe.url,
942
+ projectedLineCount: probe.projectedLineCount,
943
+ observedChars: probe.observedChars,
944
+ truncated: probe.truncated,
945
+ });
946
+
947
+ if (probe.status === 'auth_required') {
948
+ const observedSchedule = saveSchedule(controllerHome, {
949
+ ...latest,
950
+ lastTriggeredAt: timestamp,
951
+ lastOccurrenceId: occurrenceId,
952
+ lastObservationAt: timestamp,
953
+ lastObservationStatus: 'auth_required',
954
+ consecutiveFailures: 0,
955
+ nextEligibleAt: undefined,
956
+ pausedReason: undefined,
957
+ });
958
+ if (args.wake_on_auth_required !== false) {
959
+ const authPrompt = typeof args.auth_required_prompt === 'string'
960
+ ? args.auth_required_prompt
961
+ : typeof args.continuation_prompt === 'string'
962
+ ? args.continuation_prompt
963
+ : `Scheduled browser watcher ${schedule.scheduleId} detected that authentication is required (${probe.authReason ?? 'login marker'}). Inspect the external dependency and request user login only if it cannot be restored safely.`;
964
+ if (workId) {
965
+ const wakeArgs = { ...args, continuation_prompt: authPrompt };
966
+ return executeExternalControllerWake(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, authReason: probe.authReason });
967
+ }
968
+ const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
969
+ if (controllerType !== 'chatgpt') {
970
+ return decideOccurrence(controllerHome, observedSchedule, occurrence, 'operation_blocked', 'skipped', 'STANDALONE_BROWSER_KEEPALIVE_CHATGPT_REQUIRED', observationEvidence);
971
+ }
972
+ const dispatched = await runStandaloneChatgptPrompt({
973
+ controllerHome,
974
+ repoId: schedule.repoId,
975
+ scopeId: `schedule:${schedule.scheduleId}`,
976
+ prompt: authPrompt,
977
+ browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
978
+ conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
979
+ });
980
+ if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_STANDALONE_PROMPT_FAILED');
981
+ return decideOccurrence(
982
+ controllerHome,
983
+ observedSchedule,
984
+ occurrence,
985
+ 'execute',
986
+ 'succeeded',
987
+ 'Standalone browser keepalive auth-required prompt dispatched to ChatGPT.',
988
+ {
989
+ ...observationEvidence,
990
+ authReason: probe.authReason,
991
+ browserSessionId: dispatched.browserSessionId,
992
+ conversationUrl: dispatched.conversationUrl,
993
+ },
994
+ );
995
+ }
996
+ return decideOccurrence(controllerHome, observedSchedule, occurrence, 'operation_blocked', 'skipped', `Browser ${workId ? 'watcher' : 'keepalive'} requires authentication: ${probe.authReason ?? 'login marker matched'}`, observationEvidence);
997
+ }
998
+
999
+ if (probe.status === 'keepalive') {
1000
+ const observedSchedule = saveSchedule(controllerHome, {
1001
+ ...latest,
1002
+ lastTriggeredAt: timestamp,
1003
+ lastOccurrenceId: occurrenceId,
1004
+ lastObservationAt: timestamp,
1005
+ lastObservationStatus: 'keepalive',
1006
+ consecutiveFailures: 0,
1007
+ consecutiveNoops: 0,
1008
+ nextEligibleAt: undefined,
1009
+ pausedReason: undefined,
1010
+ });
1011
+ return decideOccurrence(controllerHome, observedSchedule, occurrence, 'execute', 'succeeded', 'Browser session keepalive refreshed successfully.', observationEvidence);
1012
+ }
1013
+
1014
+ const observation = classifyScheduledBrowserObservation(
1015
+ latest.lastObservationFingerprint,
1016
+ probe.fingerprint,
1017
+ args.wake_on_first_observation === true,
1018
+ );
1019
+ const changed = observation.status === 'changed';
1020
+ const shouldWake = args.wake_on_change !== false && observation.shouldWake;
1021
+ const observationStatus: RepositorySchedule['lastObservationStatus'] = observation.status;
1022
+ const observedSchedule = saveSchedule(controllerHome, {
609
1023
  ...latest,
610
1024
  lastTriggeredAt: timestamp,
611
1025
  lastOccurrenceId: occurrenceId,
1026
+ lastObservationAt: timestamp,
1027
+ lastObservationFingerprint: probe.fingerprint,
1028
+ lastObservationChangedAt: changed ? timestamp : latest.lastObservationChangedAt,
1029
+ lastObservationStatus: observationStatus,
612
1030
  consecutiveFailures: 0,
1031
+ consecutiveNoops: shouldWake ? 0 : (latest.consecutiveNoops ?? 0) + 1,
613
1032
  nextEligibleAt: undefined,
614
1033
  pausedReason: undefined,
615
1034
  });
616
- return saveOccurrence(controllerHome, {
617
- ...wakeDecision,
618
- status: 'succeeded',
619
- reason: `External Controller wake started ${launched.controllerType} pid=${String(launched.pid ?? 'unknown')} for Work ${workId}.`,
620
- });
1035
+
1036
+ if (!shouldWake) {
1037
+ const reason = observation.status === 'baseline'
1038
+ ? 'Browser watcher baseline recorded; no Controller wake was emitted.'
1039
+ : 'Browser watcher observation is unchanged; no Controller wake was emitted.';
1040
+ return decideOccurrence(controllerHome, observedSchedule, occurrence, 'nothing_to_do', 'skipped', reason, { ...observationEvidence, observationStatus });
1041
+ }
1042
+
1043
+ const wakeArgs = {
1044
+ ...args,
1045
+ continuation_prompt: typeof args.continuation_prompt === 'string'
1046
+ ? args.continuation_prompt
1047
+ : `Scheduled browser watcher ${schedule.scheduleId} detected a changed external observation. Read the current external state, correlate it with Work ${workId}, and continue only the bounded Work objective.`,
1048
+ };
1049
+ return executeExternalControllerWake(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, observationStatus });
621
1050
  } catch (error) {
622
1051
  const reason = error instanceof Error ? error.message : String(error);
623
1052
  const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
@@ -625,21 +1054,21 @@ export async function evaluateSchedule(
625
1054
  decision: 'execute',
626
1055
  reason,
627
1056
  handoff: {
628
- title: `External Controller wake ${occurrence.occurrenceId} failed`,
629
- summary: 'Forge recorded the schedule trigger but could not wake the configured external Controller.',
1057
+ title: `Browser watcher occurrence ${occurrence.occurrenceId} failed`,
1058
+ summary: 'A bounded browser probe failed before Forge could compare the external observation.',
630
1059
  reason,
631
1060
  creationReason: 'repeated_infrastructure_failure',
632
- blockingDecision: 'Repair the external Controller/browser launch path or update the saved session reference.',
633
- recommendedDecision: 'Inspect launcher/browser readiness, then retrigger this bounded continuation.',
634
- recommendedPrompt: `Resume Work ${workId} manually, inspect failed wake occurrence ${occurrence.occurrenceId}, and repair the configured external Controller launch path before unattended continuation resumes.`,
635
- statusSummary: 'Scheduled external Controller wake failed.',
636
- blockedBy: ['external_controller_wake_failed'],
637
- attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, `controller:${controllerType}`],
1061
+ blockingDecision: 'Repair browser/session readiness or update the watcher target before unattended probing resumes.',
1062
+ recommendedDecision: 'Inspect the browser plugin/session and retrigger the watcher after the target is readable.',
1063
+ recommendedPrompt: `Inspect browser watcher schedule ${schedule.scheduleId} for Work ${workId}, repair the failed browser probe, then retrigger one bounded occurrence.`,
1064
+ statusSummary: 'Scheduled browser watcher failed.',
1065
+ blockedBy: ['browser_probe_failed'],
1066
+ attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, 'operation:browser_probe'],
638
1067
  },
639
1068
  });
640
1069
  const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
641
1070
  saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
642
- return failed.occurrence ?? saveOccurrence(controllerHome, { ...wakeDecision, status: 'failed', reason });
1071
+ return failed.occurrence ?? saveOccurrence(controllerHome, { ...occurrence, status: 'failed', decision: 'execute', reason });
643
1072
  }
644
1073
  }
645
1074