@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
@@ -0,0 +1,158 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { resolveTrustedExecutable } from '../../shared/trusted-executable';
4
+
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ export interface GithubIssueWatchIssue {
8
+ number: number;
9
+ title: string;
10
+ state: 'open' | 'closed';
11
+ updatedAt: string;
12
+ url: string;
13
+ }
14
+
15
+ export interface GithubIssueWatchState {
16
+ schemaVersion: 1;
17
+ observedAt: string;
18
+ signatures: Record<string, string>;
19
+ }
20
+
21
+ export interface GithubIssueWatchObservation {
22
+ status: 'baseline' | 'unchanged' | 'changed';
23
+ shouldWake: boolean;
24
+ changedOpenIssues: GithubIssueWatchIssue[];
25
+ issues: GithubIssueWatchIssue[];
26
+ nextState: GithubIssueWatchState;
27
+ }
28
+
29
+ function stringValue(value: unknown): string | undefined {
30
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
31
+ }
32
+
33
+ function boundedNumber(value: unknown, fallback: number, min: number, max: number): number {
34
+ return typeof value === 'number' && Number.isFinite(value)
35
+ ? Math.max(min, Math.min(Math.trunc(value), max))
36
+ : fallback;
37
+ }
38
+
39
+ function issueSignature(issue: GithubIssueWatchIssue): string {
40
+ return `${issue.state}:${issue.updatedAt}`;
41
+ }
42
+
43
+ function normalizeIssue(value: unknown): GithubIssueWatchIssue | undefined {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
45
+ const record = value as Record<string, unknown>;
46
+ const number = typeof record.number === 'number' && Number.isFinite(record.number) ? Math.trunc(record.number) : undefined;
47
+ const title = stringValue(record.title);
48
+ const state = record.state === 'open' || record.state === 'closed' ? record.state : undefined;
49
+ const updatedAt = stringValue(record.updatedAt);
50
+ const url = stringValue(record.url);
51
+ if (!number || !title || !state || !updatedAt || !url) return undefined;
52
+ return { number, title, state, updatedAt, url };
53
+ }
54
+
55
+ export function normalizeGithubIssueWatchState(value: unknown): GithubIssueWatchState | undefined {
56
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
57
+ const record = value as Record<string, unknown>;
58
+ const signaturesValue = record.signatures;
59
+ if (!signaturesValue || typeof signaturesValue !== 'object' || Array.isArray(signaturesValue)) return undefined;
60
+ const signatures = Object.fromEntries(Object.entries(signaturesValue as Record<string, unknown>)
61
+ .filter((entry): entry is [string, string] => typeof entry[1] === 'string'));
62
+ return {
63
+ schemaVersion: 1,
64
+ observedAt: stringValue(record.observedAt) ?? new Date(0).toISOString(),
65
+ signatures,
66
+ };
67
+ }
68
+
69
+ export function classifyGithubIssueWatchObservation(
70
+ issues: GithubIssueWatchIssue[],
71
+ previousState: GithubIssueWatchState | undefined,
72
+ options: { since?: string; wakeOnFirstObservation?: boolean; observedAt?: string } = {},
73
+ ): GithubIssueWatchObservation {
74
+ const observedAt = options.observedAt ?? new Date().toISOString();
75
+ const signatures = Object.fromEntries(issues.map((issue) => [String(issue.number), issueSignature(issue)]));
76
+ const firstObservation = !previousState;
77
+ const sinceMs = options.since ? Date.parse(options.since) : Number.NaN;
78
+ const changedOpenIssues = issues.filter((issue) => {
79
+ if (issue.state !== 'open') return false;
80
+ if (!firstObservation) return previousState.signatures[String(issue.number)] !== issueSignature(issue);
81
+ if (options.wakeOnFirstObservation === true) return true;
82
+ return Number.isFinite(sinceMs) && Date.parse(issue.updatedAt) > sinceMs;
83
+ });
84
+ return {
85
+ status: firstObservation ? 'baseline' : changedOpenIssues.length > 0 ? 'changed' : 'unchanged',
86
+ shouldWake: changedOpenIssues.length > 0,
87
+ changedOpenIssues,
88
+ issues,
89
+ nextState: { schemaVersion: 1, observedAt, signatures },
90
+ };
91
+ }
92
+
93
+ const DEFAULT_GH_EXECUTABLE_CANDIDATES = [
94
+ '/opt/homebrew/bin/gh',
95
+ '/usr/local/bin/gh',
96
+ '/usr/bin/gh',
97
+ ] as const;
98
+
99
+ export function resolveGithubIssueWatchExecutable(
100
+ args: Record<string, unknown>,
101
+ env: NodeJS.ProcessEnv = process.env,
102
+ preferredPaths: readonly string[] = DEFAULT_GH_EXECUTABLE_CANDIDATES,
103
+ ): string {
104
+ const configured = stringValue(args.gh_executable) ?? stringValue(env.FORGE_GH_EXECUTABLE);
105
+ const resolved = resolveTrustedExecutable({
106
+ name: 'gh',
107
+ configured,
108
+ preferredPaths,
109
+ env,
110
+ });
111
+ if (resolved.configuredInvalid) {
112
+ throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_INVALID: gh_executable/FORGE_GH_EXECUTABLE must be an absolute executable path.');
113
+ }
114
+ if (!resolved.executable) {
115
+ throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_NOT_FOUND: GitHub CLI was not found in trusted Homebrew/system locations or PATH.');
116
+ }
117
+ return resolved.executable;
118
+ }
119
+
120
+ export async function executeScheduledGithubIssueWatch(input: {
121
+ repoRoot: string;
122
+ args: Record<string, unknown>;
123
+ observedAt?: string;
124
+ }): Promise<GithubIssueWatchObservation> {
125
+ const repository = stringValue(input.args.github_repository);
126
+ if (!repository || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
127
+ throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_REPOSITORY_REQUIRED');
128
+ }
129
+ const limit = boundedNumber(input.args.issue_limit, 50, 10, 100);
130
+ const timeoutMs = boundedNumber(input.args.timeout_ms, 15_000, 1_000, 60_000);
131
+ try {
132
+ const ghExecutable = resolveGithubIssueWatchExecutable(input.args);
133
+ const result = await execFileAsync(ghExecutable, [
134
+ 'api',
135
+ '-H', 'Accept: application/vnd.github+json',
136
+ `repos/${repository}/issues?state=all&sort=updated&direction=desc&per_page=${limit}`,
137
+ '--jq', 'map(select(.pull_request|not)) | map({number,title,state,updatedAt:.updated_at,url:.html_url})',
138
+ ], {
139
+ cwd: input.repoRoot,
140
+ encoding: 'utf8',
141
+ timeout: timeoutMs,
142
+ maxBuffer: 2 * 1024 * 1024,
143
+ });
144
+ const parsed = JSON.parse(String(result.stdout)) as unknown;
145
+ if (!Array.isArray(parsed)) throw new Error('GitHub issue response was not an array.');
146
+ const issues = parsed.map(normalizeIssue).filter((issue): issue is GithubIssueWatchIssue => Boolean(issue));
147
+ const previousState = normalizeGithubIssueWatchState(input.args.issue_watch_state);
148
+ return classifyGithubIssueWatchObservation(issues, previousState, {
149
+ since: stringValue(input.args.issue_watch_since),
150
+ wakeOnFirstObservation: input.args.wake_on_first_observation === true,
151
+ observedAt: input.observedAt,
152
+ });
153
+ } catch (error) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ if (message.startsWith('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_')) throw error;
156
+ throw new Error(`SCHEDULE_GITHUB_ISSUE_WATCH_FAILED: ${message}`);
157
+ }
158
+ }
@@ -11,14 +11,21 @@ import {
11
11
 
12
12
  const TERMINAL_OCCURRENCE_STATUSES = new Set<ScheduleOccurrence['status']>(['succeeded', 'failed', 'shadowed', 'skipped']);
13
13
 
14
- function computeScheduleFailureState(schedule: ReturnType<typeof getSchedule>, nextFailures: number): Pick<ReturnType<typeof getSchedule>, 'consecutiveFailures' | 'nextEligibleAt' | 'enabled' | 'pausedReason'> {
15
- const shouldPause = nextFailures >= schedule.policy.maxFailures;
14
+ function computeScheduleBackoff(schedule: ReturnType<typeof getSchedule>, nextFailures: number): string {
16
15
  const backoffBase = Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1);
17
16
  const backoffMax = Math.max(backoffBase, schedule.policy.backoffMaxMinutes ?? 24 * 60);
18
- const backoffMinutes = Math.min(backoffMax, backoffBase * (2 ** Math.max(0, nextFailures - 1)));
17
+ // Persistent readiness failures may span hours or days. Cap the exponent so
18
+ // the calculation stays bounded even while the observable failure count grows.
19
+ const exponent = Math.min(16, Math.max(0, nextFailures - 1));
20
+ const backoffMinutes = Math.min(backoffMax, backoffBase * (2 ** exponent));
21
+ return new Date(Date.now() + backoffMinutes * 60_000).toISOString();
22
+ }
23
+
24
+ function computeScheduleFailureState(schedule: ReturnType<typeof getSchedule>, nextFailures: number): Pick<ReturnType<typeof getSchedule>, 'consecutiveFailures' | 'nextEligibleAt' | 'enabled' | 'pausedReason'> {
25
+ const shouldPause = nextFailures >= schedule.policy.maxFailures;
19
26
  return {
20
27
  consecutiveFailures: nextFailures,
21
- nextEligibleAt: new Date(Date.now() + backoffMinutes * 60_000).toISOString(),
28
+ nextEligibleAt: computeScheduleBackoff(schedule, nextFailures),
22
29
  enabled: shouldPause ? false : schedule.enabled,
23
30
  pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : undefined,
24
31
  };
@@ -68,6 +75,43 @@ export function applyScheduleFailure(
68
75
  return { schedule: nextSchedule, occurrence: nextOccurrence };
69
76
  }
70
77
 
78
+ export function applyScheduleRetryableFailure(
79
+ controllerHome: string,
80
+ scheduleId: string,
81
+ repoId: string,
82
+ occurrenceId: string,
83
+ options: {
84
+ outcome: 'failed' | 'skipped';
85
+ decision: ScheduleDecisionType;
86
+ reason: string;
87
+ countFailure?: boolean;
88
+ },
89
+ ): { schedule: ReturnType<typeof getSchedule>; occurrence?: ScheduleOccurrence } {
90
+ const schedule = getSchedule(controllerHome, repoId, scheduleId);
91
+ const occurrence = getOccurrence(controllerHome, repoId, occurrenceId);
92
+ let nextOccurrence = occurrence;
93
+ if (occurrence && !TERMINAL_OCCURRENCE_STATUSES.has(occurrence.status)) {
94
+ nextOccurrence = saveOccurrence(controllerHome, {
95
+ ...occurrence,
96
+ status: options.outcome,
97
+ decision: options.decision,
98
+ reason: options.reason,
99
+ });
100
+ }
101
+ const countFailure = options.countFailure !== false;
102
+ const nextFailures = countFailure ? schedule.consecutiveFailures + 1 : schedule.consecutiveFailures;
103
+ const nextSchedule = saveSchedule(controllerHome, {
104
+ ...schedule,
105
+ consecutiveFailures: nextFailures,
106
+ nextEligibleAt: computeScheduleBackoff(schedule, Math.max(1, nextFailures)),
107
+ // Retryable readiness/semantic-wait failures never turn a durable schedule
108
+ // into a manual-resume task. Respect a concurrent explicit pause, however.
109
+ enabled: schedule.enabled,
110
+ pausedReason: schedule.enabled ? undefined : schedule.pausedReason,
111
+ });
112
+ return { schedule: nextSchedule, occurrence: nextOccurrence };
113
+ }
114
+
71
115
  export function markScheduledExecutionRunning(controllerHome: string, job: ExecutionJob): void {
72
116
  if (job.type !== 'scheduled-occurrence') return;
73
117
  const occurrenceId = typeof job.payload.occurrenceId === 'string' ? job.payload.occurrenceId : undefined;
@@ -1,9 +1,14 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
- import { existsSync, readdirSync } from 'fs';
2
+ import { existsSync, readdirSync, rmSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
5
5
  import { withControllerLock } from '../../../cli/repositories/locks';
6
- import { createHandoffItem, getHandoffItem } from '../../control-plane/facade/handoff-inbox-store';
6
+ import {
7
+ createHandoffItem,
8
+ getHandoffItem,
9
+ listHandoffItems,
10
+ resolveHandoffItem,
11
+ } from '../../control-plane/facade/handoff-inbox-store';
7
12
  import { readJsonFile, writeJsonAtomic } from '../../shared/json-files';
8
13
  import { appendRuntimeEvent } from '../../evidence/event-ledger';
9
14
  import type { RepositorySchedule, ScheduleDecision, ScheduleOccurrence } from './types';
@@ -159,17 +164,120 @@ export function listSchedules(controllerHome: string, repoId: string): Repositor
159
164
  } catch { return []; }
160
165
  }
161
166
 
167
+ export function deleteSchedule(controllerHome: string, repoId: string, scheduleId: string): RepositorySchedule {
168
+ return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `schedule-${scheduleId}` }, `delete-schedule:${scheduleId}`, () => {
169
+ const current = getSchedule(controllerHome, repoId, scheduleId);
170
+ if (current.enabled) throw new Error(`SCHEDULE_DELETE_REQUIRES_PAUSED: ${scheduleId}`);
171
+ if (listActiveOccurrences(controllerHome, repoId, scheduleId).length > 0) {
172
+ throw new Error(`SCHEDULE_DELETE_ACTIVE_OCCURRENCE: ${scheduleId}`);
173
+ }
174
+ rmSync(schedulePath(controllerHome, repoId, scheduleId), { force: true });
175
+ const requestRecordPath = requestPath(controllerHome, repoId, current.requestId);
176
+ if (existsSync(requestRecordPath)) {
177
+ const record = readJsonFile<ScheduleRequestRecord>(requestRecordPath);
178
+ if (record.scheduleId === scheduleId) rmSync(requestRecordPath, { force: true });
179
+ }
180
+ appendRuntimeEvent(controllerHome, {
181
+ repoId,
182
+ entityType: 'schedule',
183
+ entityId: scheduleId,
184
+ eventType: 'schedule_deleted',
185
+ requestId: current.requestId,
186
+ revision: current.revision + 1,
187
+ data: { name: current.name },
188
+ });
189
+ return current;
190
+ }, 10_000);
191
+ }
192
+
193
+ function scheduleFailureClass(reason: string): string {
194
+ const trimmed = reason.trim();
195
+ const explicitCode = trimmed.match(/^([A-Z][A-Z0-9_]{2,})(?::|\b)/)?.[1];
196
+ if (explicitCode) return explicitCode;
197
+ const prefix = (trimmed.split(':', 1)[0] ?? trimmed)
198
+ .replace(/\s+/g, ' ')
199
+ .trim()
200
+ .toLowerCase()
201
+ .slice(0, 120);
202
+ return prefix || 'unknown_failure';
203
+ }
204
+
205
+ function activeScheduleFailureHandoff(
206
+ controllerHome: string,
207
+ repoId: string,
208
+ scheduleId: string,
209
+ creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
210
+ reason: string,
211
+ ) {
212
+ const failureClass = scheduleFailureClass(reason);
213
+ return listHandoffItems({ controllerHome, repoId, status: 'active', limit: 100 }).find((item) => (
214
+ item.currentState?.taskId === scheduleId
215
+ && item.creationReason === creationReason
216
+ && scheduleFailureClass(item.reason) === failureClass
217
+ ));
218
+ }
219
+
220
+ function stableScheduleFailureHandoffId(
221
+ controllerHome: string,
222
+ repoId: string,
223
+ scheduleId: string,
224
+ creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
225
+ reason: string,
226
+ occurrenceId: string,
227
+ ): string {
228
+ const digest = createHash('sha256')
229
+ .update(`${scheduleId}:${creationReason}:${scheduleFailureClass(reason)}`)
230
+ .digest('hex')
231
+ .slice(0, 20);
232
+ const baseId = `schedule-failure-${digest}`;
233
+ if (!getHandoffItem({ controllerHome, repoId }, baseId)) return baseId;
234
+ // A previous failure streak may already have resolved the stable item. Handoff
235
+ // records are immutable apart from lifecycle status, so use one new epoch id;
236
+ // subsequent failures in this streak will discover and reuse the active item.
237
+ const epoch = createHash('sha256').update(occurrenceId).digest('hex').slice(0, 8);
238
+ return `${baseId}-${epoch}`;
239
+ }
240
+
241
+ function resolveRecoveredScheduleFailureHandoffs(controllerHome: string, occurrence: ScheduleOccurrence): void {
242
+ try {
243
+ const active = listHandoffItems({ controllerHome, repoId: occurrence.repoId, status: 'active', limit: 100 })
244
+ .filter((item) => (
245
+ item.currentState?.taskId === occurrence.scheduleId
246
+ && item.creationReason === 'repeated_infrastructure_failure'
247
+ ));
248
+ for (const item of active) {
249
+ try {
250
+ resolveHandoffItem(
251
+ { controllerHome, repoId: occurrence.repoId },
252
+ item.id,
253
+ {
254
+ decision: `Schedule ${occurrence.scheduleId} recovered on occurrence ${occurrence.occurrenceId}.`,
255
+ resolver: 'schedule-runtime',
256
+ },
257
+ );
258
+ } catch {
259
+ // The succeeded occurrence remains authoritative. Inbox cleanup is
260
+ // best-effort and must never turn schedule recovery into a failure.
261
+ }
262
+ }
263
+ } catch {
264
+ // Preserve successful Schedule settlement even if Inbox storage is unavailable.
265
+ }
266
+ }
267
+
162
268
  export function saveOccurrence(controllerHome: string, occurrence: ScheduleOccurrence): ScheduleOccurrence {
163
- return withControllerLock(controllerHome, { scope: 'task', repoId: occurrence.repoId, taskId: `schedule-${occurrence.scheduleId}` }, `save-occurrence:${occurrence.occurrenceId}`, () => {
269
+ const next = withControllerLock(controllerHome, { scope: 'task', repoId: occurrence.repoId, taskId: `schedule-${occurrence.scheduleId}` }, `save-occurrence:${occurrence.occurrenceId}`, () => {
164
270
  const existingPath = occurrencePath(controllerHome, occurrence.repoId, occurrence.occurrenceId);
165
271
  const previous = existsSync(existingPath) ? readJsonFile<ScheduleOccurrence>(existingPath) : undefined;
166
- const next = { ...occurrence, revision: (previous?.revision ?? occurrence.revision ?? 0) + 1, updatedAt: new Date().toISOString() };
167
- writeJsonAtomic(existingPath, next);
168
- upsertOccurrenceIndexUnlocked(controllerHome, next);
169
- const schedule = getSchedule(controllerHome, next.repoId, next.scheduleId);
170
- appendRuntimeEvent(controllerHome, { repoId: next.repoId, entityType: 'occurrence', entityId: next.occurrenceId, eventType: `occurrence_${next.status}`, requestId: `${schedule.requestId}:${next.windowKey}`, revision: next.revision, correlationId: next.scheduleId, data: { decision: next.decision, jobId: next.jobId, reason: next.reason } });
171
- return next;
272
+ const saved = { ...occurrence, revision: (previous?.revision ?? occurrence.revision ?? 0) + 1, updatedAt: new Date().toISOString() };
273
+ writeJsonAtomic(existingPath, saved);
274
+ upsertOccurrenceIndexUnlocked(controllerHome, saved);
275
+ const schedule = getSchedule(controllerHome, saved.repoId, saved.scheduleId);
276
+ appendRuntimeEvent(controllerHome, { repoId: saved.repoId, entityType: 'occurrence', entityId: saved.occurrenceId, eventType: `occurrence_${saved.status}`, requestId: `${schedule.requestId}:${saved.windowKey}`, revision: saved.revision, correlationId: saved.scheduleId, data: { decision: saved.decision, jobId: saved.jobId, reason: saved.reason } });
277
+ return saved;
172
278
  }, 10_000);
279
+ if (next.status === 'succeeded') resolveRecoveredScheduleFailureHandoffs(controllerHome, next);
280
+ return next;
173
281
  }
174
282
 
175
283
  export function getOccurrence(controllerHome: string, repoId: string, occurrenceId: string): ScheduleOccurrence | undefined {
@@ -184,7 +292,20 @@ export function recordScheduleOccurrenceHandoff(
184
292
  input: ScheduleOccurrenceHandoffInput,
185
293
  ): ScheduleOccurrence {
186
294
  if (occurrence.handoffId) return occurrence;
187
- const handoffId = `schedule-${occurrence.occurrenceId}`;
295
+ const reusable = input.creationReason === 'repeated_infrastructure_failure'
296
+ ? activeScheduleFailureHandoff(controllerHome, occurrence.repoId, schedule.scheduleId, input.creationReason, input.reason)
297
+ : undefined;
298
+ if (reusable) return saveOccurrence(controllerHome, { ...occurrence, handoffId: reusable.id });
299
+ const handoffId = input.creationReason === 'repeated_infrastructure_failure'
300
+ ? stableScheduleFailureHandoffId(
301
+ controllerHome,
302
+ occurrence.repoId,
303
+ schedule.scheduleId,
304
+ input.creationReason,
305
+ input.reason,
306
+ occurrence.occurrenceId,
307
+ )
308
+ : `schedule-${occurrence.occurrenceId}`;
188
309
  const evidenceRefs: NonNullable<ScheduleOccurrenceHandoffInput['evidenceRefs']> = [
189
310
  { title: `Occurrence ${occurrence.occurrenceId}`, summary: occurrence.reason ?? input.reason, detailLevel: 'summary' as const },
190
311
  ];
@@ -10,10 +10,8 @@ export type ScheduleTriggerType =
10
10
  | 'manual';
11
11
 
12
12
  export interface ScheduleCondition {
13
- kind: 'repository_clean' | 'job_succeeded' | 'job_terminal' | 'candidate_observation_threshold';
13
+ kind: 'repository_clean' | 'job_succeeded' | 'job_terminal';
14
14
  jobId?: string;
15
- semanticKey?: string;
16
- observationThreshold?: number;
17
15
  }
18
16
 
19
17
  export interface ScheduleTrigger {
@@ -73,6 +71,11 @@ export interface RepositorySchedule {
73
71
  consecutiveNoops?: number;
74
72
  nextEligibleAt?: string;
75
73
  pausedReason?: string;
74
+ /** Last deterministic external observation for browser or issue watchers. */
75
+ lastObservationAt?: string;
76
+ lastObservationFingerprint?: string;
77
+ lastObservationChangedAt?: string;
78
+ lastObservationStatus?: 'baseline' | 'unchanged' | 'changed' | 'keepalive' | 'auth_required';
76
79
  }
77
80
 
78
81
  export type ScheduleDecisionType =
@@ -19,9 +19,11 @@ import type {
19
19
  } from './types';
20
20
 
21
21
  export type ContinuationControllerType = 'chatgpt' | 'codex' | 'claude' | 'grok';
22
+ export type WorkScheduleMode = 'continuation' | 'browser_watch' | 'browser_keepalive';
22
23
 
23
24
  export interface WorkContinuationScheduleInput {
24
- workId: string;
25
+ workId?: string;
26
+ scheduleMode?: WorkScheduleMode;
25
27
  controllerType?: ContinuationControllerType;
26
28
  executable?: string;
27
29
  launchArgs?: string[];
@@ -30,6 +32,18 @@ export interface WorkContinuationScheduleInput {
30
32
  browserSessionId?: string;
31
33
  conversationUrl?: string;
32
34
  continuationPrompt?: string;
35
+ probeUrl?: string;
36
+ probeBrowserSessionId?: string;
37
+ probeSelector?: string;
38
+ probeMaxChars?: number;
39
+ probeTimeoutMs?: number;
40
+ includeTerms?: string[];
41
+ ignorePatterns?: string[];
42
+ loginUrlTerms?: string[];
43
+ loginTextTerms?: string[];
44
+ wakeOnFirstObservation?: boolean;
45
+ wakeOnAuthRequired?: boolean;
46
+ authRequiredPrompt?: string;
33
47
  scheduleName?: string;
34
48
  requestId?: string;
35
49
  triggerType?: ScheduleTriggerType;
@@ -70,7 +84,7 @@ function activeWork(controllerHome: string, repoId: string, workId: string): Wor
70
84
  }
71
85
 
72
86
  function continuationSchedule(schedule: RepositorySchedule): RepositorySchedule {
73
- if (schedule.action.operation !== 'external_controller_wake') {
87
+ if (!['external_controller_wake', 'browser_probe'].includes(schedule.action.operation)) {
74
88
  throw new Error(`SCHEDULE_NOT_WORK_CONTINUATION: ${schedule.scheduleId}`);
75
89
  }
76
90
  return schedule;
@@ -92,8 +106,9 @@ function normalizedTrigger(input: WorkContinuationScheduleInput): ScheduleTrigge
92
106
  }
93
107
 
94
108
  function wakeArguments(input: WorkContinuationScheduleInput, controllerType: ContinuationControllerType): Record<string, unknown> {
109
+ const workId = input.workId?.trim();
95
110
  return {
96
- work_id: input.workId.trim(),
111
+ ...(workId ? { work_id: workId } : {}),
97
112
  controller_type: controllerType,
98
113
  ...(input.executable?.trim() ? { executable: input.executable.trim() } : {}),
99
114
  ...(input.launchArgs ? { launch_args: input.launchArgs.map(String) } : {}),
@@ -105,55 +120,120 @@ function wakeArguments(input: WorkContinuationScheduleInput, controllerType: Con
105
120
  };
106
121
  }
107
122
 
123
+ function probeArguments(input: WorkContinuationScheduleInput, controllerType: ContinuationControllerType, keepaliveOnly: boolean): Record<string, unknown> {
124
+ if (!input.probeUrl?.trim() && !input.probeBrowserSessionId?.trim()) {
125
+ throw new Error('SCHEDULE_BROWSER_PROBE_TARGET_REQUIRED');
126
+ }
127
+ return {
128
+ ...wakeArguments(input, controllerType),
129
+ ...(input.probeUrl?.trim() ? { probe_url: input.probeUrl.trim() } : {}),
130
+ ...(input.probeBrowserSessionId?.trim() ? { probe_session_id: input.probeBrowserSessionId.trim() } : {}),
131
+ ...(input.probeSelector?.trim() ? { selector: input.probeSelector.trim() } : {}),
132
+ ...(input.probeMaxChars !== undefined ? { max_chars: Math.max(256, Math.min(Math.trunc(input.probeMaxChars), 100_000)) } : {}),
133
+ ...(input.probeTimeoutMs !== undefined ? { timeout_ms: Math.max(1_000, Math.min(Math.trunc(input.probeTimeoutMs), 120_000)) } : {}),
134
+ ...(input.includeTerms ? { include_terms: input.includeTerms.map(String).filter(Boolean) } : {}),
135
+ ...(input.ignorePatterns ? { ignore_patterns: input.ignorePatterns.map(String).filter(Boolean) } : {}),
136
+ ...(input.loginUrlTerms ? { login_url_terms: input.loginUrlTerms.map(String).filter(Boolean) } : {}),
137
+ ...(input.loginTextTerms ? { login_text_terms: input.loginTextTerms.map(String).filter(Boolean) } : {}),
138
+ ...(input.wakeOnFirstObservation === true ? { wake_on_first_observation: true } : {}),
139
+ ...(input.wakeOnAuthRequired === false ? { wake_on_auth_required: false } : {}),
140
+ ...(input.authRequiredPrompt?.trim() ? { auth_required_prompt: input.authRequiredPrompt.trim() } : {}),
141
+ keepalive_only: keepaliveOnly,
142
+ wake_on_change: !keepaliveOnly,
143
+ };
144
+ }
145
+
108
146
  export function createWorkContinuationSchedule(
109
147
  controllerHome: string,
110
148
  repoId: string,
111
149
  input: WorkContinuationScheduleInput,
112
- ): { schedule: RepositorySchedule; work: WorkContract } {
113
- const work = activeWork(controllerHome, repoId, input.workId);
150
+ ): { schedule: RepositorySchedule; work?: WorkContract } {
114
151
  const controllerType = input.controllerType ?? 'chatgpt';
152
+ const scheduleMode = input.scheduleMode ?? 'continuation';
153
+ const requestedWorkId = input.workId?.trim();
154
+ if (!requestedWorkId && scheduleMode !== 'browser_keepalive') throw new Error('WORK_ID_REQUIRED');
155
+ if (!requestedWorkId && scheduleMode === 'browser_keepalive' && controllerType !== 'chatgpt') throw new Error('STANDALONE_BROWSER_KEEPALIVE_CHATGPT_REQUIRED');
156
+ const work = requestedWorkId ? activeWork(controllerHome, repoId, requestedWorkId) : undefined;
115
157
  const trigger = normalizedTrigger(input);
116
- const actionArguments = wakeArguments(input, controllerType);
117
- assertAutomatedOperationAllowed('external_controller_wake', actionArguments);
118
- const name = input.scheduleName?.trim() || `Continue Work ${work.workId}`;
158
+ const operation = scheduleMode === 'continuation' ? 'external_controller_wake' : 'browser_probe';
159
+ const actionArguments = scheduleMode === 'continuation'
160
+ ? wakeArguments(input, controllerType)
161
+ : probeArguments(input, controllerType, scheduleMode === 'browser_keepalive');
162
+ assertAutomatedOperationAllowed(operation, actionArguments);
163
+ const name = input.scheduleName?.trim()
164
+ || (scheduleMode === 'browser_watch' ? `Watch external state for Work ${work!.workId}`
165
+ : scheduleMode === 'browser_keepalive'
166
+ ? (work ? `Keep browser session alive for Work ${work.workId}` : 'Keep browser session alive')
167
+ : `Continue Work ${work!.workId}`);
168
+ const policy = {
169
+ maxActiveOccurrences: 1,
170
+ maxFailures: input.maxFailures !== undefined ? Math.max(1, Math.trunc(input.maxFailures)) : 3,
171
+ cooldownMinutes: input.cooldownMinutes !== undefined ? Math.max(0, Math.trunc(input.cooldownMinutes)) : 120,
172
+ dailyBudgetMinutes: input.dailyBudgetMinutes !== undefined ? Math.max(1, Math.trunc(input.dailyBudgetMinutes)) : 180,
173
+ shadowMode: input.shadowMode !== false,
174
+ backoffBaseMinutes: input.backoffBaseMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffBaseMinutes)) : 5,
175
+ backoffMaxMinutes: input.backoffMaxMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffMaxMinutes)) : 24 * 60,
176
+ };
177
+ const stopConditions = input.stopConditions ?? (work ? ['work_terminal', 'human_review_required', 'external_blocker'] : []);
178
+
179
+ // A durable Work has one authoritative continuation lane. Changing cadence,
180
+ // prompt, controller, or policy updates that lane instead of minting another
181
+ // Workflow. Browser watches/keepalives are intentionally excluded because one
182
+ // Work may legitimately observe several independent external targets.
183
+ if (scheduleMode === 'continuation') {
184
+ const existing = listSchedules(controllerHome, repoId)
185
+ .filter((candidate) => candidate.action.operation === 'external_controller_wake')
186
+ .filter((candidate) => candidate.action.arguments?.work_id === work!.workId);
187
+ const authoritative = [...existing].reverse().find((candidate) => candidate.enabled) ?? existing.at(-1);
188
+ if (authoritative) {
189
+ const schedule = saveSchedule(controllerHome, {
190
+ ...authoritative,
191
+ name,
192
+ enabled: true,
193
+ trigger,
194
+ policy,
195
+ action: { operation, target: 'runtime', arguments: actionArguments, resourceClaims: [] },
196
+ stopConditions,
197
+ pausedReason: undefined,
198
+ });
199
+ for (const duplicate of existing) {
200
+ if (duplicate.scheduleId === schedule.scheduleId || !duplicate.enabled) continue;
201
+ saveSchedule(controllerHome, {
202
+ ...duplicate,
203
+ enabled: false,
204
+ pausedReason: `Superseded by authoritative continuation ${schedule.scheduleId} for Work ${work!.workId}.`,
205
+ });
206
+ }
207
+ return { schedule, work };
208
+ }
209
+ }
210
+
119
211
  const semantic = JSON.stringify({
120
212
  repoId,
121
- workId: work.workId,
213
+ workId: work?.workId,
122
214
  controllerType,
215
+ scheduleMode,
123
216
  trigger,
124
217
  name,
125
218
  actionArguments,
126
- policy: {
127
- maxFailures: input.maxFailures,
128
- cooldownMinutes: input.cooldownMinutes,
129
- dailyBudgetMinutes: input.dailyBudgetMinutes,
130
- shadowMode: input.shadowMode,
131
- backoffBaseMinutes: input.backoffBaseMinutes,
132
- backoffMaxMinutes: input.backoffMaxMinutes,
133
- },
134
- stopConditions: input.stopConditions,
219
+ policy,
220
+ stopConditions,
135
221
  });
136
222
  const requestId = input.requestId?.trim()
137
- || `work-continuation:${repoId}:${work.workId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`;
223
+ || (work
224
+ ? `work-continuation:${repoId}:${work.workId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`
225
+ : `browser-keepalive:${repoId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`);
138
226
  const schedule = createSchedule(controllerHome, {
139
227
  requestId,
140
228
  repoId,
141
229
  name,
142
230
  enabled: true,
143
231
  trigger,
144
- policy: {
145
- maxActiveOccurrences: 1,
146
- maxFailures: input.maxFailures !== undefined ? Math.max(1, Math.trunc(input.maxFailures)) : 3,
147
- cooldownMinutes: input.cooldownMinutes !== undefined ? Math.max(0, Math.trunc(input.cooldownMinutes)) : 120,
148
- dailyBudgetMinutes: input.dailyBudgetMinutes !== undefined ? Math.max(1, Math.trunc(input.dailyBudgetMinutes)) : 180,
149
- shadowMode: input.shadowMode !== false,
150
- backoffBaseMinutes: input.backoffBaseMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffBaseMinutes)) : 5,
151
- backoffMaxMinutes: input.backoffMaxMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffMaxMinutes)) : 24 * 60,
152
- },
153
- // The schedule only wakes a Controller. The Controller acquires Work/resource
154
- // ownership separately, so the wake itself must not claim repository writes.
155
- action: { operation: 'external_controller_wake', target: 'runtime', arguments: actionArguments, resourceClaims: [] },
156
- stopConditions: input.stopConditions ?? ['work_terminal', 'human_review_required', 'external_blocker'],
232
+ policy,
233
+ // Work-bound schedules only perform a deterministic wake or a bounded read-only
234
+ // browser probe. Controller/Work write ownership is acquired separately.
235
+ action: { operation, target: 'runtime', arguments: actionArguments, resourceClaims: [] },
236
+ stopConditions,
157
237
  });
158
238
  return { schedule, work };
159
239
  }
@@ -164,7 +244,7 @@ export function listWorkContinuationSchedules(
164
244
  options: WorkContinuationScheduleListOptions = {},
165
245
  ): { schedules: RepositorySchedule[]; occurrences?: ScheduleOccurrence[] } {
166
246
  const schedules = listSchedules(controllerHome, repoId)
167
- .filter((entry) => entry.action.operation === 'external_controller_wake')
247
+ .filter((entry) => ['external_controller_wake', 'browser_probe'].includes(entry.action.operation))
168
248
  .filter((entry) => !options.workId || entry.action.arguments?.work_id === options.workId);
169
249
  if (!options.includeOccurrences) return { schedules };
170
250
  const scheduleIds = new Set(schedules.map((entry) => entry.scheduleId));
@@ -204,7 +284,13 @@ export function resumeWorkContinuationSchedule(controllerHome: string, repoId: s
204
284
  const schedule = continuationSchedule(getSchedule(controllerHome, repoId, scheduleId));
205
285
  const workId = typeof schedule.action.arguments?.work_id === 'string' ? schedule.action.arguments.work_id : '';
206
286
  activeWork(controllerHome, repoId, workId);
207
- return saveSchedule(controllerHome, { ...schedule, enabled: true, pausedReason: undefined });
287
+ return saveSchedule(controllerHome, {
288
+ ...schedule,
289
+ enabled: true,
290
+ pausedReason: undefined,
291
+ consecutiveFailures: 0,
292
+ nextEligibleAt: undefined,
293
+ });
208
294
  }
209
295
 
210
296
  export async function triggerWorkContinuationSchedule(