@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
@@ -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
+ }
@@ -5,22 +5,29 @@ import {
5
5
  getSchedule,
6
6
  recordScheduleOccurrenceHandoff,
7
7
  saveOccurrence,
8
- saveSchedule,
8
+ updateSchedule,
9
9
  type ScheduleOccurrenceHandoffInput,
10
10
  } from './store';
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
- pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : undefined,
30
+ pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : schedule.enabled ? undefined : schedule.pausedReason,
24
31
  };
25
32
  }
26
33
 
@@ -55,16 +62,58 @@ export function applyScheduleFailure(
55
62
 
56
63
  const countFailure = options.countFailure !== false;
57
64
  const nextSchedule = options.pauseReason
58
- ? saveSchedule(controllerHome, {
59
- ...schedule,
65
+ ? updateSchedule(controllerHome, repoId, scheduleId, (current) => ({
60
66
  enabled: false,
61
67
  pausedReason: options.pauseReason,
62
68
  nextEligibleAt: undefined,
63
- consecutiveFailures: countFailure ? schedule.consecutiveFailures + 1 : schedule.consecutiveFailures,
64
- })
69
+ consecutiveFailures: countFailure ? current.consecutiveFailures + 1 : current.consecutiveFailures,
70
+ }))
65
71
  : countFailure
66
- ? saveSchedule(controllerHome, { ...schedule, ...computeScheduleFailureState(schedule, schedule.consecutiveFailures + 1) })
67
- : schedule;
72
+ ? updateSchedule(controllerHome, repoId, scheduleId, (current) => computeScheduleFailureState(current, current.consecutiveFailures + 1))
73
+ : getSchedule(controllerHome, repoId, scheduleId);
74
+ return { schedule: nextSchedule, occurrence: nextOccurrence };
75
+ }
76
+
77
+ export function applyScheduleRetryableFailure(
78
+ controllerHome: string,
79
+ scheduleId: string,
80
+ repoId: string,
81
+ occurrenceId: string,
82
+ options: {
83
+ outcome: 'failed' | 'skipped';
84
+ decision: ScheduleDecisionType;
85
+ reason: string;
86
+ countFailure?: boolean;
87
+ },
88
+ ): { schedule: ReturnType<typeof getSchedule>; occurrence?: ScheduleOccurrence } {
89
+ const occurrence = getOccurrence(controllerHome, repoId, occurrenceId);
90
+ let nextOccurrence = occurrence;
91
+ if (occurrence && !TERMINAL_OCCURRENCE_STATUSES.has(occurrence.status)) {
92
+ nextOccurrence = saveOccurrence(controllerHome, {
93
+ ...occurrence,
94
+ status: options.outcome,
95
+ decision: options.decision,
96
+ reason: options.reason,
97
+ });
98
+ }
99
+ const countFailure = options.countFailure !== false;
100
+ const nextSchedule = updateSchedule(controllerHome, repoId, scheduleId, (current) => {
101
+ const nextFailures = countFailure ? current.consecutiveFailures + 1 : current.consecutiveFailures;
102
+ const failureState = countFailure && current.enabled
103
+ ? computeScheduleFailureState(current, nextFailures)
104
+ : {
105
+ consecutiveFailures: nextFailures,
106
+ nextEligibleAt: countFailure
107
+ ? current.nextEligibleAt
108
+ : computeScheduleBackoff(current, Math.max(1, nextFailures)),
109
+ enabled: current.enabled,
110
+ pausedReason: current.enabled ? undefined : current.pausedReason,
111
+ };
112
+ // One transient fault backs off and re-arms. Repeated faults have crossed
113
+ // the already-declared failure budget and must stop before they consume
114
+ // further controller sessions or browser interactions.
115
+ return failureState;
116
+ });
68
117
  return { schedule: nextSchedule, occurrence: nextOccurrence };
69
118
  }
70
119
 
@@ -98,7 +147,6 @@ export function settleScheduledExecution(
98
147
  const occurrenceId = typeof job.payload.occurrenceId === 'string' ? job.payload.occurrenceId : undefined;
99
148
  if (!scheduleId || !occurrenceId) return;
100
149
  try {
101
- const schedule = getSchedule(controllerHome, job.repoId, scheduleId);
102
150
  const occurrence = getOccurrence(controllerHome, job.repoId, occurrenceId);
103
151
  if (occurrence && !['succeeded', 'failed', 'shadowed', 'skipped'].includes(occurrence.status)) {
104
152
  saveOccurrence(controllerHome, {
@@ -131,12 +179,11 @@ export function settleScheduledExecution(
131
179
  });
132
180
  return;
133
181
  }
134
- saveSchedule(controllerHome, {
135
- ...schedule,
182
+ updateSchedule(controllerHome, job.repoId, scheduleId, (current) => ({
136
183
  consecutiveFailures: 0,
137
184
  nextEligibleAt: undefined,
138
- pausedReason: undefined,
139
- });
185
+ ...(current.enabled ? { pausedReason: undefined } : {}),
186
+ }));
140
187
  } catch {
141
188
  // Job terminal state remains authoritative even if an old schedule record
142
189
  // has already been removed. Reconciliation must not be blocked by it.
@@ -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';
@@ -23,6 +28,7 @@ interface ScheduleRequestRecord {
23
28
  createdAt: string;
24
29
  }
25
30
  type CreateScheduleInput = Omit<RepositorySchedule, 'schemaVersion' | 'revision' | 'scheduleId' | 'createdAt' | 'updatedAt' | 'consecutiveFailures'>;
31
+ type ScheduleMutableUpdate = Partial<Omit<RepositorySchedule, 'schemaVersion' | 'revision' | 'scheduleId' | 'repoId' | 'requestId' | 'createdAt' | 'updatedAt'>>;
26
32
 
27
33
  export interface ScheduleOccurrenceHandoffInput {
28
34
  title: string;
@@ -57,6 +63,30 @@ function occurrenceIndexPath(controllerHome: string, repoId: string): string {
57
63
  function requestPath(controllerHome: string, repoId: string, requestId: string): string {
58
64
  return join(schedulesRoot(controllerHome, repoId), 'indexes', 'requests', `${createHash('sha256').update(requestId).digest('hex')}.json`);
59
65
  }
66
+
67
+ const EXTERNAL_CONTROLLER_WAKE_MAX_FAILURES = 3;
68
+ const EXTERNAL_CONTROLLER_WAKE_DAILY_BUDGET_MINUTES = 60;
69
+ const EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES = 10;
70
+
71
+ function boundedSchedulePolicy(schedule: RepositorySchedule): RepositorySchedule['policy'] {
72
+ const policy = {
73
+ ...schedule.policy,
74
+ backoffBaseMinutes: Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1),
75
+ backoffMaxMinutes: Math.max(1, schedule.policy.backoffMaxMinutes ?? 24 * 60),
76
+ };
77
+ if (schedule.action.operation !== 'external_controller_wake') return policy;
78
+ // A Work-bound controller wake is expensive and can affect the user's
79
+ // browser. No schedule payload may convert a transient tool outage into an
80
+ // all-day retry loop. Larger work needs an explicit future user action after
81
+ // the bounded lane records useful failure evidence.
82
+ return {
83
+ ...policy,
84
+ maxFailures: Math.max(1, Math.min(policy.maxFailures, EXTERNAL_CONTROLLER_WAKE_MAX_FAILURES)),
85
+ dailyBudgetMinutes: Math.max(1, Math.min(policy.dailyBudgetMinutes, EXTERNAL_CONTROLLER_WAKE_DAILY_BUDGET_MINUTES)),
86
+ cooldownMinutes: Math.max(policy.cooldownMinutes, EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES),
87
+ backoffBaseMinutes: Math.max(policy.backoffBaseMinutes, EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES),
88
+ };
89
+ }
60
90
  function canonical(value: unknown): unknown {
61
91
  if (Array.isArray(value)) return value.map(canonical);
62
92
  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value as Record<string, unknown>)
@@ -131,22 +161,62 @@ export function getSchedule(controllerHome: string, repoId: string, scheduleId:
131
161
  ...schedule,
132
162
  revision: Number.isFinite(schedule.revision) ? schedule.revision : 1,
133
163
  requestId: schedule.requestId || `legacy-schedule:${schedule.scheduleId}`,
134
- policy: {
135
- ...schedule.policy,
136
- backoffBaseMinutes: Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1),
137
- backoffMaxMinutes: Math.max(1, schedule.policy.backoffMaxMinutes ?? 24 * 60),
138
- },
164
+ policy: boundedSchedulePolicy(schedule),
139
165
  consecutiveNoops: Math.max(0, schedule.consecutiveNoops ?? 0),
140
166
  };
141
167
  }
142
168
 
169
+ function persistScheduleRevision(
170
+ controllerHome: string,
171
+ current: RepositorySchedule,
172
+ candidate: RepositorySchedule,
173
+ ): RepositorySchedule {
174
+ const next: RepositorySchedule = {
175
+ ...candidate,
176
+ schemaVersion: current.schemaVersion,
177
+ revision: current.revision + 1,
178
+ scheduleId: current.scheduleId,
179
+ repoId: current.repoId,
180
+ requestId: current.requestId,
181
+ createdAt: current.createdAt,
182
+ updatedAt: new Date().toISOString(),
183
+ };
184
+ writeJsonAtomic(schedulePath(controllerHome, next.repoId, next.scheduleId), next);
185
+ appendRuntimeEvent(controllerHome, { repoId: next.repoId, entityType: 'schedule', entityId: next.scheduleId, eventType: 'schedule_updated', requestId: next.requestId, revision: next.revision, data: { enabled: next.enabled, pausedReason: next.pausedReason } });
186
+ return next;
187
+ }
188
+
189
+ /**
190
+ * Persist a caller-owned Schedule snapshot only when its revision is still the
191
+ * current authority. User/configuration mutations use this path so stale
192
+ * writers fail closed instead of overwriting a newer decision.
193
+ */
143
194
  export function saveSchedule(controllerHome: string, schedule: RepositorySchedule): RepositorySchedule {
144
195
  return withControllerLock(controllerHome, { scope: 'task', repoId: schedule.repoId, taskId: `schedule-${schedule.scheduleId}` }, `save-schedule:${schedule.scheduleId}`, () => {
145
196
  const current = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
146
- const next = { ...schedule, revision: current.revision + 1, updatedAt: new Date().toISOString() };
147
- writeJsonAtomic(schedulePath(controllerHome, next.repoId, next.scheduleId), next);
148
- appendRuntimeEvent(controllerHome, { repoId: next.repoId, entityType: 'schedule', entityId: next.scheduleId, eventType: 'schedule_updated', requestId: next.requestId, revision: next.revision, data: { enabled: next.enabled, pausedReason: next.pausedReason } });
149
- return next;
197
+ if (schedule.revision !== current.revision) {
198
+ throw new Error(`SCHEDULE_REVISION_CONFLICT: ${schedule.scheduleId}:expected=${schedule.revision}:actual=${current.revision}`);
199
+ }
200
+ return persistScheduleRevision(controllerHome, current, schedule);
201
+ }, 10_000);
202
+ }
203
+
204
+ /**
205
+ * Apply runtime-owned settlement/observation fields to the latest Schedule
206
+ * authority while holding the Schedule lock. Identity and revision fields are
207
+ * never writable through this path. Long-running occurrences use this instead
208
+ * of replaying a stale whole-record snapshot after user state may have changed.
209
+ */
210
+ export function updateSchedule(
211
+ controllerHome: string,
212
+ repoId: string,
213
+ scheduleId: string,
214
+ update: (current: RepositorySchedule) => ScheduleMutableUpdate,
215
+ ): RepositorySchedule {
216
+ return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `schedule-${scheduleId}` }, `update-schedule:${scheduleId}`, () => {
217
+ const current = getSchedule(controllerHome, repoId, scheduleId);
218
+ const changes = update(current);
219
+ return persistScheduleRevision(controllerHome, current, { ...current, ...changes });
150
220
  }, 10_000);
151
221
  }
152
222
 
@@ -159,17 +229,124 @@ export function listSchedules(controllerHome: string, repoId: string): Repositor
159
229
  } catch { return []; }
160
230
  }
161
231
 
232
+ export function deleteSchedule(controllerHome: string, repoId: string, scheduleId: string): RepositorySchedule {
233
+ return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `schedule-${scheduleId}` }, `delete-schedule:${scheduleId}`, () => {
234
+ const current = getSchedule(controllerHome, repoId, scheduleId);
235
+ if (current.enabled) throw new Error(`SCHEDULE_DELETE_REQUIRES_PAUSED: ${scheduleId}`);
236
+ if (listActiveOccurrences(controllerHome, repoId, scheduleId).length > 0) {
237
+ throw new Error(`SCHEDULE_DELETE_ACTIVE_OCCURRENCE: ${scheduleId}`);
238
+ }
239
+ rmSync(schedulePath(controllerHome, repoId, scheduleId), { force: true });
240
+ const requestRecordPath = requestPath(controllerHome, repoId, current.requestId);
241
+ if (existsSync(requestRecordPath)) {
242
+ const record = readJsonFile<ScheduleRequestRecord>(requestRecordPath);
243
+ if (record.scheduleId === scheduleId) rmSync(requestRecordPath, { force: true });
244
+ }
245
+ appendRuntimeEvent(controllerHome, {
246
+ repoId,
247
+ entityType: 'schedule',
248
+ entityId: scheduleId,
249
+ eventType: 'schedule_deleted',
250
+ requestId: current.requestId,
251
+ revision: current.revision + 1,
252
+ data: { name: current.name },
253
+ });
254
+ return current;
255
+ }, 10_000);
256
+ }
257
+
258
+ function scheduleFailureClass(reason: string): string {
259
+ const trimmed = reason.trim();
260
+ const explicitCode = trimmed.match(/^([A-Z][A-Z0-9_]{2,})(?::|\b)/)?.[1];
261
+ if (explicitCode) return explicitCode;
262
+ const prefix = (trimmed.split(':', 1)[0] ?? trimmed)
263
+ .replace(/\s+/g, ' ')
264
+ .trim()
265
+ .toLowerCase()
266
+ .slice(0, 120);
267
+ return prefix || 'unknown_failure';
268
+ }
269
+
270
+ function activeScheduleFailureHandoff(
271
+ controllerHome: string,
272
+ repoId: string,
273
+ scheduleId: string,
274
+ creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
275
+ reason: string,
276
+ ) {
277
+ const failureClass = scheduleFailureClass(reason);
278
+ return listHandoffItems({ controllerHome, repoId, status: 'active', limit: 100 }).find((item) => (
279
+ item.currentState?.taskId === scheduleId
280
+ && item.creationReason === creationReason
281
+ && scheduleFailureClass(item.reason) === failureClass
282
+ ));
283
+ }
284
+
285
+ function stableScheduleFailureHandoffId(
286
+ controllerHome: string,
287
+ repoId: string,
288
+ scheduleId: string,
289
+ creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
290
+ reason: string,
291
+ occurrenceId: string,
292
+ ): string {
293
+ const digest = createHash('sha256')
294
+ .update(`${scheduleId}:${creationReason}:${scheduleFailureClass(reason)}`)
295
+ .digest('hex')
296
+ .slice(0, 20);
297
+ const baseId = `schedule-failure-${digest}`;
298
+ if (!getHandoffItem({ controllerHome, repoId }, baseId)) return baseId;
299
+ // A previous failure streak may already have resolved the stable item. Handoff
300
+ // records are immutable apart from lifecycle status, so use one new epoch id;
301
+ // subsequent failures in this streak will discover and reuse the active item.
302
+ const epoch = createHash('sha256').update(occurrenceId).digest('hex').slice(0, 8);
303
+ return `${baseId}-${epoch}`;
304
+ }
305
+
306
+ function resolveRecoveredScheduleFailureHandoffs(controllerHome: string, occurrence: ScheduleOccurrence): void {
307
+ try {
308
+ const active = listHandoffItems({ controllerHome, repoId: occurrence.repoId, status: 'active', limit: 100 })
309
+ .filter((item) => (
310
+ item.currentState?.taskId === occurrence.scheduleId
311
+ && item.creationReason === 'repeated_infrastructure_failure'
312
+ ));
313
+ for (const item of active) {
314
+ try {
315
+ resolveHandoffItem(
316
+ { controllerHome, repoId: occurrence.repoId },
317
+ item.id,
318
+ {
319
+ decision: `Schedule ${occurrence.scheduleId} recovered on occurrence ${occurrence.occurrenceId}.`,
320
+ resolver: 'schedule-runtime',
321
+ },
322
+ );
323
+ } catch {
324
+ // The succeeded occurrence remains authoritative. Inbox cleanup is
325
+ // best-effort and must never turn schedule recovery into a failure.
326
+ }
327
+ }
328
+ } catch {
329
+ // Preserve successful Schedule settlement even if Inbox storage is unavailable.
330
+ }
331
+ }
332
+
162
333
  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}`, () => {
334
+ const next = withControllerLock(controllerHome, { scope: 'task', repoId: occurrence.repoId, taskId: `schedule-${occurrence.scheduleId}` }, `save-occurrence:${occurrence.occurrenceId}`, () => {
164
335
  const existingPath = occurrencePath(controllerHome, occurrence.repoId, occurrence.occurrenceId);
165
336
  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;
337
+ const expectedRevision = previous?.revision ?? 0;
338
+ if (occurrence.revision !== expectedRevision) {
339
+ throw new Error(`SCHEDULE_OCCURRENCE_REVISION_CONFLICT: ${occurrence.occurrenceId}:expected=${occurrence.revision}:actual=${expectedRevision}`);
340
+ }
341
+ const saved = { ...occurrence, revision: expectedRevision + 1, updatedAt: new Date().toISOString() };
342
+ writeJsonAtomic(existingPath, saved);
343
+ upsertOccurrenceIndexUnlocked(controllerHome, saved);
344
+ const schedule = getSchedule(controllerHome, saved.repoId, saved.scheduleId);
345
+ 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 } });
346
+ return saved;
172
347
  }, 10_000);
348
+ if (next.status === 'succeeded') resolveRecoveredScheduleFailureHandoffs(controllerHome, next);
349
+ return next;
173
350
  }
174
351
 
175
352
  export function getOccurrence(controllerHome: string, repoId: string, occurrenceId: string): ScheduleOccurrence | undefined {
@@ -184,7 +361,20 @@ export function recordScheduleOccurrenceHandoff(
184
361
  input: ScheduleOccurrenceHandoffInput,
185
362
  ): ScheduleOccurrence {
186
363
  if (occurrence.handoffId) return occurrence;
187
- const handoffId = `schedule-${occurrence.occurrenceId}`;
364
+ const reusable = input.creationReason === 'repeated_infrastructure_failure'
365
+ ? activeScheduleFailureHandoff(controllerHome, occurrence.repoId, schedule.scheduleId, input.creationReason, input.reason)
366
+ : undefined;
367
+ if (reusable) return saveOccurrence(controllerHome, { ...occurrence, handoffId: reusable.id });
368
+ const handoffId = input.creationReason === 'repeated_infrastructure_failure'
369
+ ? stableScheduleFailureHandoffId(
370
+ controllerHome,
371
+ occurrence.repoId,
372
+ schedule.scheduleId,
373
+ input.creationReason,
374
+ input.reason,
375
+ occurrence.occurrenceId,
376
+ )
377
+ : `schedule-${occurrence.occurrenceId}`;
188
378
  const evidenceRefs: NonNullable<ScheduleOccurrenceHandoffInput['evidenceRefs']> = [
189
379
  { title: `Occurrence ${occurrence.occurrenceId}`, summary: occurrence.reason ?? input.reason, detailLevel: 'summary' as const },
190
380
  ];
@@ -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,7 +71,7 @@ export interface RepositorySchedule {
73
71
  consecutiveNoops?: number;
74
72
  nextEligibleAt?: string;
75
73
  pausedReason?: string;
76
- /** Last deterministic external observation for browser_watch/browser_keepalive. */
74
+ /** Last deterministic external observation for browser or issue watchers. */
77
75
  lastObservationAt?: string;
78
76
  lastObservationFingerprint?: string;
79
77
  lastObservationChangedAt?: string;