@moretea-labs/forge 1.6.0 → 1.7.1

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