@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
@@ -5,6 +5,7 @@ export * from './work-task-projection';
5
5
  export * from './work-continuation';
6
6
  export * from './controller-session-store';
7
7
  export * from './plan-contract-store';
8
+ export * from './semantic-admission';
8
9
  export * from './facade-result';
9
10
  export * from './suggested-actions';
10
11
  export * from './capability-registry';
@@ -3,12 +3,21 @@ import { join } from 'path';
3
3
  import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
4
4
  import { withControllerLock } from '../../../cli/repositories/locks';
5
5
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
6
+ import {
7
+ normalizePlanScopeKey,
8
+ resolvePlanAdmission,
9
+ withPlanAdmissionLock,
10
+ withPlanAdmissionLockAsync,
11
+ type PlanAdmissionRelation,
12
+ type PlanAdmissionResolution,
13
+ } from './semantic-admission';
6
14
  import {
7
15
  listControlPlaneRecords,
8
16
  readControlPlaneRecordWithinTransaction,
9
17
  withControlPlaneTransaction,
10
18
  writeControlPlaneRecordWithinTransaction,
11
19
  } from '../persistence/sqlite-store';
20
+ import { readRequirement } from '../persistence/requirement-store';
12
21
  import {
13
22
  isTerminalPlanContractStatus,
14
23
  type EvidenceRef,
@@ -46,6 +55,13 @@ export interface CreatePlanContractInput {
46
55
  evidenceRefs?: EvidenceRef[];
47
56
  }
48
57
 
58
+ export interface AdmitPlanContractInput extends CreatePlanContractInput {
59
+ planRelation?: PlanAdmissionRelation;
60
+ relatedPlanId?: string;
61
+ }
62
+
63
+ export type AdmitPlanContractResult = PlanAdmissionResolution;
64
+
49
65
  export interface PlanContractSummary {
50
66
  planId: string;
51
67
  repoId: string;
@@ -68,7 +84,7 @@ function bounded(values: readonly string[] | undefined, limit: number, maxLength
68
84
  }
69
85
 
70
86
  function normalizeScopeKey(value: string): string {
71
- return sanitizeFileComponent(value).toLowerCase().slice(0, 160);
87
+ return normalizePlanScopeKey(value) || 'unknown';
72
88
  }
73
89
 
74
90
  function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep {
@@ -115,6 +131,14 @@ function updatePlanContract(
115
131
  );
116
132
  }
117
133
 
134
+ function assertRequirementReference(options: PlanContractStoreOptions, requirementId: string | undefined): void {
135
+ const normalized = requirementId?.trim();
136
+ if (!normalized || !options.controllerHome) return;
137
+ if (!readRequirement({ controllerHome: options.controllerHome, now: options.now }, normalized)) {
138
+ throw new Error(`PLAN_REQUIREMENT_NOT_FOUND: ${normalized}`);
139
+ }
140
+ }
141
+
118
142
  export function planContractRoot(location: PlanContractStoreLocation): string {
119
143
  if (location.root) {
120
144
  mkdirSync(location.root, { recursive: true });
@@ -205,11 +229,10 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
205
229
  return store;
206
230
  }
207
231
 
208
- export function createPlanContract(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
209
- const at = nowIso(options);
232
+ function buildPlanContract(input: CreatePlanContractInput, at: string): PlanContract {
210
233
  const planId = sanitizeFileComponent(input.planId);
211
234
  if (!String(input.planId ?? '').trim() || planId === 'unknown') throw new Error('plan_id is required');
212
- const plan: PlanContract = {
235
+ return {
213
236
  schemaVersion: 1,
214
237
  planId,
215
238
  repoId: input.repoId,
@@ -229,6 +252,12 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
229
252
  createdAt: at,
230
253
  updatedAt: at,
231
254
  };
255
+ }
256
+
257
+ function createPlanContractUnlocked(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
258
+ assertRequirementReference(options, input.requirementId);
259
+ const at = nowIso(options);
260
+ const plan = buildPlanContract(input, at);
232
261
  const store = readPlanContractStore(options);
233
262
  if (store.contracts.some((existing) => existing.planId === plan.planId)) {
234
263
  throw new Error(`plan contract already exists: ${plan.planId}`);
@@ -237,6 +266,77 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
237
266
  return plan;
238
267
  }
239
268
 
269
+ function replacePlanContractUnlocked(
270
+ options: PlanContractStoreOptions,
271
+ predecessorId: string,
272
+ input: CreatePlanContractInput,
273
+ ): PlanContract {
274
+ assertRequirementReference(options, input.requirementId);
275
+ const at = nowIso(options);
276
+ const store = readPlanContractStore(options);
277
+ const predecessorKey = sanitizeFileComponent(predecessorId);
278
+ const predecessorIndex = store.contracts.findIndex((contract) => contract.planId === predecessorKey);
279
+ if (predecessorIndex < 0) throw new Error(`plan contract not found: ${predecessorKey}`);
280
+ const predecessor = store.contracts[predecessorIndex]!;
281
+ if (isTerminalPlanContractStatus(predecessor.status)) throw new Error(`plan contract ${predecessor.planId} is terminal (${predecessor.status})`);
282
+ const successor = buildPlanContract(input, at);
283
+ if (successor.planId === predecessor.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
284
+ if (successor.repoId !== predecessor.repoId) throw new Error('PLAN_SUCCESSOR_REPOSITORY_MISMATCH');
285
+ if (store.contracts.some((existing) => existing.planId === successor.planId)) {
286
+ throw new Error(`plan contract already exists: ${successor.planId}`);
287
+ }
288
+ const conflictingScope = store.contracts.find((existing, index) => index !== predecessorIndex
289
+ && !isTerminalPlanContractStatus(existing.status)
290
+ && existing.scopeKey === successor.scopeKey);
291
+ if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${successor.scopeKey}:${conflictingScope.planId}`);
292
+ const contracts = [...store.contracts];
293
+ contracts[predecessorIndex] = { ...predecessor, status: 'superseded', supersededBy: successor.planId, updatedAt: at };
294
+ writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts: [successor, ...contracts] });
295
+ return successor;
296
+ }
297
+
298
+ export function createPlanContract(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
299
+ return withPlanAdmissionLock(options, () => {
300
+ const store = readPlanContractStore(options);
301
+ const planId = sanitizeFileComponent(input.planId);
302
+ if (store.contracts.some((existing) => existing.planId === planId)) {
303
+ throw new Error(`plan contract already exists: ${planId}`);
304
+ }
305
+ const scopeKey = normalizeScopeKey(input.scopeKey);
306
+ const existingScopeAuthority = scopeKey === 'unknown' ? undefined : store.contracts.find((existing) =>
307
+ !isTerminalPlanContractStatus(existing.status) && existing.scopeKey === scopeKey);
308
+ if (existingScopeAuthority) {
309
+ throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${scopeKey}:${existingScopeAuthority.planId}`);
310
+ }
311
+ return createPlanContractUnlocked(options, input);
312
+ });
313
+ }
314
+
315
+ function admitPlanContractUnlocked(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
316
+ assertRequirementReference(options, input.requirementId);
317
+ const activePlans = listPlanContracts({ ...options, status: 'active', limit: 100 });
318
+ const resolution = resolvePlanAdmission(activePlans, {
319
+ requirementId: input.requirementId,
320
+ scopeKey: input.scopeKey,
321
+ planRelation: input.planRelation,
322
+ relatedPlanId: input.relatedPlanId,
323
+ });
324
+ if (resolution.admissionDecision === 'extend_existing' && resolution.plan) {
325
+ const successor = replacePlanContractUnlocked(options, resolution.plan.planId, input);
326
+ return { ...resolution, admissionDecision: 'create_new', plan: successor };
327
+ }
328
+ if (resolution.admissionDecision !== 'create_new') return resolution;
329
+ return { ...resolution, plan: createPlanContractUnlocked(options, input) };
330
+ }
331
+
332
+ export function admitPlanContract(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
333
+ return withPlanAdmissionLock(options, () => admitPlanContractUnlocked(options, input));
334
+ }
335
+
336
+ export async function admitPlanContractAsync(options: PlanContractStoreOptions, input: AdmitPlanContractInput): Promise<AdmitPlanContractResult> {
337
+ return await withPlanAdmissionLockAsync(options, () => admitPlanContractUnlocked(options, input));
338
+ }
339
+
240
340
  export function getPlanContract(options: PlanContractStoreOptions, planId: string): PlanContract | undefined {
241
341
  return readPlanContractStore(options).contracts.find((contract) => contract.planId === sanitizeFileComponent(planId));
242
342
  }
@@ -301,11 +401,12 @@ function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]):
301
401
  return [...new Set(errors)];
302
402
  }
303
403
 
304
- export function approvePlanContract(options: PlanContractStoreOptions, planId: string): PlanContract {
404
+ function approvePlanContractUnlocked(options: PlanContractStoreOptions, planId: string): PlanContract {
305
405
  const store = readPlanContractStore(options);
306
406
  const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
307
407
  if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
308
408
  const current = store.contracts[index];
409
+ assertRequirementReference(options, current.requirementId);
309
410
  if (current.status !== 'draft' && current.status !== 'reviewing') throw new Error(`plan contract ${current.planId} cannot be approved from ${current.status}`);
310
411
  const errors = approvalErrors(current, store.contracts);
311
412
  if (errors.length > 0) throw new Error(`plan contract ${current.planId} cannot be approved: ${errors.join('; ')}`);
@@ -317,20 +418,33 @@ export function approvePlanContract(options: PlanContractStoreOptions, planId: s
317
418
  return next;
318
419
  }
319
420
 
421
+ export function approvePlanContract(options: PlanContractStoreOptions, planId: string): PlanContract {
422
+ return withPlanAdmissionLock(options, () => approvePlanContractUnlocked(options, planId));
423
+ }
424
+
425
+ export async function approvePlanContractAsync(options: PlanContractStoreOptions, planId: string): Promise<PlanContract> {
426
+ return await withPlanAdmissionLockAsync(options, () => approvePlanContractUnlocked(options, planId));
427
+ }
428
+
320
429
  export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string): PlanContract {
321
- const store = readPlanContractStore(options);
322
- const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
323
- if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
324
- const current = store.contracts[index];
325
- if (isTerminalPlanContractStatus(current.status)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
326
- const replacement = sanitizeFileComponent(supersededBy);
327
- if (!replacement || replacement === 'unknown') throw new Error('superseded_by is required');
328
- const at = nowIso(options);
329
- const next = { ...current, status: 'superseded' as const, supersededBy: replacement, updatedAt: at };
330
- const contracts = [...store.contracts];
331
- contracts[index] = next;
332
- writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
333
- return next;
430
+ return withPlanAdmissionLock(options, () => {
431
+ const store = readPlanContractStore(options);
432
+ const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
433
+ if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
434
+ const current = store.contracts[index]!;
435
+ if (isTerminalPlanContractStatus(current.status)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
436
+ const replacement = sanitizeFileComponent(supersededBy);
437
+ if (!replacement || replacement === 'unknown') throw new Error('superseded_by is required');
438
+ if (replacement === current.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
439
+ const successor = store.contracts.find((contract) => contract.planId === replacement);
440
+ if (!successor) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND: ${replacement}`);
441
+ const at = nowIso(options);
442
+ const next = { ...current, status: 'superseded' as const, supersededBy: successor.planId, updatedAt: at };
443
+ const contracts = [...store.contracts];
444
+ contracts[index] = next;
445
+ writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
446
+ return next;
447
+ });
334
448
  }
335
449
 
336
450
  /**
@@ -343,6 +457,7 @@ export function claimPlanStepForWork(
343
457
  input: { planId: string; stepId: string; workId: string; sourceRevision: string },
344
458
  ): PlanContract {
345
459
  return updatePlanContract(options, input.planId, (current) => {
460
+ assertRequirementReference(options, current.requirementId);
346
461
  if (current.status !== 'approved' && current.status !== 'executing') {
347
462
  throw new Error(`PLAN_NOT_EXECUTABLE: ${current.planId} is ${current.status}`);
348
463
  }
@@ -363,6 +478,45 @@ export function claimPlanStepForWork(
363
478
  });
364
479
  }
365
480
 
481
+ export function repairDanglingPlanStepWorkBinding(
482
+ options: PlanContractStoreOptions,
483
+ input: { planId: string; stepId: string; expectedWorkId: string; reason: string },
484
+ ): PlanContract {
485
+ const expectedWorkId = input.expectedWorkId.trim();
486
+ const reason = input.reason.trim();
487
+ if (!expectedWorkId) throw new Error('PLAN_STEP_REPAIR_EXPECTED_WORK_REQUIRED');
488
+ if (!reason) throw new Error('PLAN_STEP_REPAIR_REASON_REQUIRED');
489
+ return updatePlanContract(options, input.planId, (current) => {
490
+ if (isTerminalPlanContractStatus(current.status)) throw new Error(`PLAN_STEP_REPAIR_PLAN_TERMINAL: ${current.planId} is ${current.status}`);
491
+ const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
492
+ if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
493
+ const step = current.steps[stepIndex];
494
+ if (step.workId !== expectedWorkId) {
495
+ throw new Error(`PLAN_STEP_REPAIR_BINDING_CHANGED: ${step.id} is bound to ${step.workId ?? 'no Work'}, expected ${expectedWorkId}`);
496
+ }
497
+ if (step.status !== 'executing') {
498
+ throw new Error(`PLAN_STEP_REPAIR_STATUS_INVALID: ${step.id} is ${step.status}`);
499
+ }
500
+ const at = nowIso(options);
501
+ const steps = [...current.steps];
502
+ steps[stepIndex] = {
503
+ ...step,
504
+ status: 'ready',
505
+ workId: undefined,
506
+ evidenceRefs: [{
507
+ title: 'dangling Work binding repaired',
508
+ summary: `${expectedWorkId}: ${reason}`.slice(0, 2_000),
509
+ detailLevel: 'summary' as const,
510
+ }, ...step.evidenceRefs].slice(0, 20),
511
+ };
512
+ // This is admission-state repair, not semantic replanning: the Plan and its
513
+ // acceptance criteria remain authoritative. Return the exact step to ready
514
+ // so a later start can atomically admit one replacement for the missing
515
+ // record without forcing a second Plan authority.
516
+ return { ...current, status: 'executing', steps, updatedAt: at };
517
+ });
518
+ }
519
+
366
520
  export function completePlanStepForWork(
367
521
  options: PlanContractStoreOptions,
368
522
  input: {
@@ -376,28 +530,59 @@ export function completePlanStepForWork(
376
530
  if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
377
531
  const step = current.steps[stepIndex];
378
532
  if (step.workId !== input.work.workId) throw new Error(`PLAN_STEP_WORK_MISMATCH: ${input.stepId}`);
379
- const completed = input.work.status === 'completed'
533
+ if (step.status === 'completed') return current;
534
+ const delivered = input.work.status === 'completed'
380
535
  && input.work.phase === 'cleanup'
381
536
  && input.work.evidenceState === 'valid'
382
537
  && Boolean(input.work.completionOutcome && input.work.completionOutcome !== 'superseded')
383
538
  && Boolean(input.work.completionReceipt);
384
539
  const failed = input.work.status === 'failed' || input.work.status === 'cancelled';
385
- if (!completed && !failed) throw new Error(`PLAN_STEP_WORK_NOT_TERMINAL: ${input.work.workId}`);
540
+ if (!delivered && !failed) throw new Error(`PLAN_STEP_WORK_NOT_TERMINAL: ${input.work.workId}`);
386
541
  const at = nowIso(options);
387
542
  const steps = [...current.steps];
388
543
  steps[stepIndex] = {
389
544
  ...step,
390
- status: completed ? 'completed' : 'validating',
391
- evidenceRefs: input.work.evidenceRefs.length > 0
392
- ? input.work.evidenceRefs.slice(0, 20)
393
- : step.evidenceRefs,
545
+ // Work completion proves machine delivery only. The Plan step remains in
546
+ // semantic validation until an explicit Controller acceptance records it.
547
+ // Failed/cancelled Work returns the slice to ready for replanning.
548
+ status: delivered ? 'validating' : 'ready',
549
+ // Successful delivery keeps the exact Work identity through semantic
550
+ // validation. Failed/cancelled Work releases the step authority so the
551
+ // replanning path cannot retain a ghost binding to a terminal Work.
552
+ workId: delivered ? step.workId : undefined,
553
+ evidenceRefs: input.work.evidenceRefs.length > 0 ? input.work.evidenceRefs.slice(0, 20) : step.evidenceRefs,
554
+ };
555
+ return { ...current, status: delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
556
+ });
557
+ }
558
+
559
+ export function acceptPlanStepEvidence(
560
+ options: PlanContractStoreOptions,
561
+ input: { planId: string; stepId: string; reviewer: string; rationale: string; acceptedSourceRevision?: string },
562
+ ): PlanContract {
563
+ const reviewer = input.reviewer.trim();
564
+ const rationale = input.rationale.trim();
565
+ if (!reviewer || !rationale) throw new Error('PLAN_STEP_SEMANTIC_ACCEPTANCE_METADATA_REQUIRED');
566
+ return updatePlanContract(options, input.planId, (current) => {
567
+ const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
568
+ if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
569
+ const step = current.steps[stepIndex];
570
+ if (step.status === 'completed') return current;
571
+ if (step.status !== 'validating') throw new Error(`PLAN_STEP_NOT_READY_FOR_SEMANTIC_ACCEPTANCE: ${step.id} is ${step.status}`);
572
+ const steps = [...current.steps];
573
+ steps[stepIndex] = {
574
+ ...step,
575
+ status: 'completed',
576
+ evidenceRefs: [{ title: 'semantic acceptance', summary: `${reviewer}: ${rationale}`, detailLevel: 'summary' as const }, ...step.evidenceRefs].slice(0, 20),
394
577
  };
395
578
  const allCompleted = steps.every((candidate) => candidate.status === 'completed');
579
+ const acceptedSourceRevision = input.acceptedSourceRevision?.trim();
396
580
  return {
397
581
  ...current,
398
- status: completed ? (allCompleted ? 'ready_to_finalize' : 'executing') : 'replanning',
582
+ ...(acceptedSourceRevision ? { sourceRevision: acceptedSourceRevision } : {}),
583
+ status: allCompleted ? 'finalized' : 'executing',
399
584
  steps,
400
- updatedAt: at,
585
+ updatedAt: nowIso(options),
401
586
  };
402
587
  });
403
588
  }
@@ -28,7 +28,9 @@ export interface RequirementBoardItem {
28
28
  maintenanceSummary?: string;
29
29
  blocker?: string;
30
30
  requiredUserDecision?: string;
31
+ /** Compatibility projection for older UI clients; derived from activePlanIds. */
31
32
  activePlanId?: string;
33
+ activePlanIds: string[];
32
34
  latestDelivery?: {
33
35
  workId: string;
34
36
  receiptId: string;
@@ -55,10 +57,14 @@ function explicitUserDecision(requirement: Requirement): string | undefined {
55
57
  return decision || undefined;
56
58
  }
57
59
 
58
- function projectedRequirementState(requirement: Requirement): RequirementState {
60
+ function activePlansForRequirement(requirementId: string, plans: readonly PlanContract[]): PlanContract[] {
61
+ return plans.filter((plan) => plan.requirementId === requirementId);
62
+ }
63
+
64
+ function projectedRequirementState(requirement: Requirement, activePlans: readonly PlanContract[]): RequirementState {
59
65
  if (requirement.state !== 'waiting_for_user') return requirement.state;
60
66
  if (explicitUserDecision(requirement)) return 'waiting_for_user';
61
- return requirement.activePlanId ? 'active' : 'planned';
67
+ return activePlans.length > 0 ? 'active' : 'planned';
62
68
  }
63
69
 
64
70
  function loadScopedWork(options: RequirementBoardOptions): WorkContract[] {
@@ -92,8 +98,10 @@ function latestDelivery(requirementId: string, work: readonly WorkContract[]): R
92
98
  };
93
99
  }
94
100
 
95
- function projectRequirement(requirement: Requirement, work: readonly WorkContract[]): RequirementBoardItem {
96
- const state = projectedRequirementState(requirement);
101
+ function projectRequirement(requirement: Requirement, work: readonly WorkContract[], plans: readonly PlanContract[] = []): RequirementBoardItem {
102
+ const activePlans = activePlansForRequirement(requirement.requirementId, plans);
103
+ const activePlanIds = activePlans.map((plan) => plan.planId);
104
+ const state = projectedRequirementState(requirement, activePlans);
97
105
  const requiredUserDecision = explicitUserDecision(requirement);
98
106
  const maintenanceSummary = requirement.needsAttention
99
107
  ? boundedText(requirement.attentionSummary, 1_000) || 'This Requirement has a maintenance finding.'
@@ -108,7 +116,8 @@ function projectRequirement(requirement: Requirement, work: readonly WorkContrac
108
116
  maintenanceSummary,
109
117
  blocker: requirement.needsAttention && state !== 'waiting_for_user' ? maintenanceSummary : undefined,
110
118
  requiredUserDecision,
111
- activePlanId: requirement.activePlanId,
119
+ activePlanId: activePlanIds[0],
120
+ activePlanIds,
112
121
  latestDelivery: latestDelivery(requirement.requirementId, work),
113
122
  updatedAt: requirement.updatedAt,
114
123
  detailPointer: {
@@ -132,7 +141,10 @@ function requirementCounts(requirements: readonly RequirementBoardItem[]): Recor
132
141
 
133
142
  export function buildRequirementBoard(options: RequirementBoardOptions): Record<string, unknown> {
134
143
  const work = loadScopedWork(options);
135
- const allRequirements = loadRequirements(options).map((requirement) => projectRequirement(requirement, work));
144
+ const activePlans = options.repoId?.trim()
145
+ ? listPlanContracts({ controllerHome: options.controllerHome, repoId: options.repoId, status: 'active', limit: 100 })
146
+ : [];
147
+ const allRequirements = loadRequirements(options).map((requirement) => projectRequirement(requirement, work, activePlans));
136
148
  const requirements = allRequirements.slice(0, DEFAULT_REQUIREMENT_LIMIT);
137
149
  const counts = requirementCounts(allRequirements);
138
150
  return {
@@ -229,9 +241,13 @@ export function buildExecutionDiagnostics(
229
241
  : [];
230
242
  const scopedPlans = options.requirementId
231
243
  ? allPlans.filter((plan) => plan.requirementId === options.requirementId
232
- || selectedRequirements.some((requirement) => requirement.activePlanId === plan.planId))
244
+ // Legacy migration compatibility only: current Plans own the forward
245
+ // requirementId relation, so the persisted Requirement pointer is not an
246
+ // execution/lifecycle authority.
247
+ || (!plan.requirementId && selectedRequirements.some((requirement) => requirement.activePlanId === plan.planId)))
233
248
  : allPlans;
234
- const requirements = selectedRequirements.map((requirement) => projectRequirement(requirement, allWork));
249
+ const activePlans = allPlans.filter((plan) => !['finalized', 'superseded', 'cancelled', 'invalidated_by_drift'].includes(plan.status));
250
+ const requirements = selectedRequirements.map((requirement) => projectRequirement(requirement, allWork, activePlans));
235
251
  const works = scopedWork.slice(0, limit).map(summarizeDiagnosticWork);
236
252
  const plans = scopedPlans.slice(0, limit).map((plan) => summarizeDiagnosticPlan(plan, options.detailLevel));
237
253
  const maintenanceFindings = requirements
@@ -0,0 +1,205 @@
1
+ import { withControllerLock, withControllerLockAsync } from '../../../cli/repositories/locks';
2
+ import { sanitizeFileComponent } from '../../shared/json-files';
3
+ import { isTerminalPlanContractStatus, type PlanContract } from './types';
4
+
5
+ export const PLAN_ADMISSION_RELATIONS = ['extend', 'parallel'] as const;
6
+ export type PlanAdmissionRelation = (typeof PLAN_ADMISSION_RELATIONS)[number];
7
+
8
+ export type PlanAdmissionReason =
9
+ | 'exact_scope_authority'
10
+ | 'requirement_relation_required'
11
+ | 'extension_target_required'
12
+ | 'extend_existing'
13
+ | 'create_new';
14
+
15
+ export interface PlanAdmissionInput {
16
+ requirementId?: string;
17
+ scopeKey: string;
18
+ planRelation?: PlanAdmissionRelation;
19
+ relatedPlanId?: string;
20
+ }
21
+
22
+ export interface PlanAdmissionResolution {
23
+ admissionDecision: 'reuse_existing' | 'resolution_required' | 'extend_existing' | 'create_new';
24
+ resolutionRequired: boolean;
25
+ reason: PlanAdmissionReason;
26
+ normalizedScopeKey: string;
27
+ plan?: PlanContract;
28
+ candidates: PlanContract[];
29
+ allowedPlanRelations?: readonly PlanAdmissionRelation[];
30
+ }
31
+
32
+ export function normalizePlanScopeKey(value: string): string {
33
+ const raw = String(value ?? '').trim();
34
+ if (!raw) return '';
35
+ const normalized = sanitizeFileComponent(raw).toLowerCase().slice(0, 160);
36
+ return normalized === 'unknown' ? '' : normalized;
37
+ }
38
+
39
+ export function resolvePlanAdmission(
40
+ plans: readonly PlanContract[],
41
+ input: PlanAdmissionInput,
42
+ ): PlanAdmissionResolution {
43
+ const activePlans = plans.filter((plan) => !isTerminalPlanContractStatus(plan.status));
44
+ const normalizedScopeKey = normalizePlanScopeKey(input.scopeKey);
45
+ const requirementId = input.requirementId?.trim() || undefined;
46
+ const requirementPlans = requirementId
47
+ ? activePlans.filter((candidate) => candidate.requirementId === requirementId)
48
+ : [];
49
+ const relatedPlanId = input.relatedPlanId?.trim() || undefined;
50
+ const explicitlyRelatedPlan = relatedPlanId
51
+ ? activePlans.find((candidate) => candidate.planId === relatedPlanId)
52
+ : undefined;
53
+ const relatedPlan = explicitlyRelatedPlan && (!requirementId || explicitlyRelatedPlan.requirementId === requirementId)
54
+ ? explicitlyRelatedPlan
55
+ : !relatedPlanId && requirementPlans.length === 1 ? requirementPlans[0] : undefined;
56
+ const exactScopeAuthority = normalizedScopeKey
57
+ ? activePlans.find((candidate) => candidate.scopeKey === normalizedScopeKey)
58
+ : undefined;
59
+
60
+ // An explicit extension is a serial replan, not a second active Plan. Resolve
61
+ // it before exact-scope reuse so a successor may retain the predecessor scope.
62
+ // If another Plan owns the requested successor scope, preserve that authority.
63
+ if (input.planRelation === 'extend') {
64
+ if (!relatedPlan) {
65
+ return {
66
+ admissionDecision: 'resolution_required',
67
+ resolutionRequired: true,
68
+ reason: 'extension_target_required',
69
+ normalizedScopeKey,
70
+ candidates: (requirementPlans.length > 0 ? requirementPlans : activePlans).slice(0, 8),
71
+ allowedPlanRelations: PLAN_ADMISSION_RELATIONS,
72
+ };
73
+ }
74
+ if (exactScopeAuthority && exactScopeAuthority.planId !== relatedPlan.planId) {
75
+ return {
76
+ admissionDecision: 'reuse_existing',
77
+ resolutionRequired: false,
78
+ reason: 'exact_scope_authority',
79
+ normalizedScopeKey,
80
+ plan: exactScopeAuthority,
81
+ candidates: [exactScopeAuthority],
82
+ };
83
+ }
84
+ return {
85
+ admissionDecision: 'extend_existing',
86
+ resolutionRequired: false,
87
+ reason: 'extend_existing',
88
+ normalizedScopeKey,
89
+ plan: relatedPlan,
90
+ candidates: requirementPlans.length > 0 ? requirementPlans.slice(0, 8) : [relatedPlan],
91
+ };
92
+ }
93
+
94
+ if (exactScopeAuthority) {
95
+ return {
96
+ admissionDecision: 'reuse_existing',
97
+ resolutionRequired: false,
98
+ reason: 'exact_scope_authority',
99
+ normalizedScopeKey,
100
+ plan: exactScopeAuthority,
101
+ candidates: [exactScopeAuthority],
102
+ };
103
+ }
104
+
105
+ if (requirementPlans.length > 0 && !input.planRelation) {
106
+ return {
107
+ admissionDecision: 'resolution_required',
108
+ resolutionRequired: true,
109
+ reason: 'requirement_relation_required',
110
+ normalizedScopeKey,
111
+ candidates: requirementPlans.slice(0, 8),
112
+ allowedPlanRelations: PLAN_ADMISSION_RELATIONS,
113
+ };
114
+ }
115
+
116
+ return {
117
+ admissionDecision: 'create_new',
118
+ resolutionRequired: false,
119
+ reason: 'create_new',
120
+ normalizedScopeKey,
121
+ candidates: requirementPlans.slice(0, 8),
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Short per-repository semantic admission critical section. The fixed task key
127
+ * keeps admission independent from repository/worktree execution locks while
128
+ * still coordinating competing controller processes for the same repository.
129
+ */
130
+ const planAdmissionLockKey = (repoId: string) => ({
131
+ scope: 'task' as const,
132
+ repoId,
133
+ taskId: 'semantic-plan-admission',
134
+ });
135
+
136
+ export function withPlanAdmissionLock<T>(
137
+ options: { controllerHome?: string; repoId?: string },
138
+ operation: () => T,
139
+ ): T {
140
+ if (!options.controllerHome?.trim() || !options.repoId?.trim()) return operation();
141
+ return withControllerLock(
142
+ options.controllerHome,
143
+ planAdmissionLockKey(options.repoId),
144
+ 'semantic-plan-admission',
145
+ operation,
146
+ );
147
+ }
148
+
149
+ /** Gateway-facing admission waits briefly for a competing controller to finish
150
+ * its tiny create/approve decision, then re-reads canonical state under lock. */
151
+ export async function withPlanAdmissionLockAsync<T>(
152
+ options: { controllerHome?: string; repoId?: string },
153
+ operation: () => T | Promise<T>,
154
+ ): Promise<T> {
155
+ if (!options.controllerHome?.trim() || !options.repoId?.trim()) return await operation();
156
+ return await withControllerLockAsync(
157
+ options.controllerHome,
158
+ planAdmissionLockKey(options.repoId),
159
+ 'semantic-plan-admission',
160
+ async () => await operation(),
161
+ undefined,
162
+ 500,
163
+ );
164
+ }
165
+
166
+ const primaryWorkAdmissionLockKey = (repoId: string) => ({
167
+ scope: 'task' as const,
168
+ repoId,
169
+ taskId: 'semantic-primary-work-admission',
170
+ });
171
+
172
+ /**
173
+ * Primary Work admission protects only canonical ownership resolution and
174
+ * persistence. It must never span workspace materialization, dependency prep,
175
+ * checks, worker launch, or the Work execution lifetime.
176
+ */
177
+ export function withPrimaryWorkAdmissionLock<T>(
178
+ options: { controllerHome?: string; repoId?: string },
179
+ operation: () => T,
180
+ ): T {
181
+ if (!options.controllerHome?.trim() || !options.repoId?.trim()) return operation();
182
+ return withControllerLock(
183
+ options.controllerHome,
184
+ primaryWorkAdmissionLockKey(options.repoId),
185
+ 'semantic-primary-work-admission',
186
+ operation,
187
+ );
188
+ }
189
+
190
+ /** Gateway callers wait briefly for a competing admission, then re-read the
191
+ * canonical Plan/Work state while holding the same short critical section. */
192
+ export async function withPrimaryWorkAdmissionLockAsync<T>(
193
+ options: { controllerHome?: string; repoId?: string },
194
+ operation: () => T | Promise<T>,
195
+ ): Promise<T> {
196
+ if (!options.controllerHome?.trim() || !options.repoId?.trim()) return await operation();
197
+ return await withControllerLockAsync(
198
+ options.controllerHome,
199
+ primaryWorkAdmissionLockKey(options.repoId),
200
+ 'semantic-primary-work-admission',
201
+ async () => await operation(),
202
+ undefined,
203
+ 500,
204
+ );
205
+ }
@@ -11,10 +11,13 @@ function expectedActionRisk(action: SuggestedNextAction): SuggestedNextAction['r
11
11
  case 'delegate':
12
12
  case 'controller_claim':
13
13
  case 'controller_release':
14
+ case 'controller_disposition':
14
15
  case 'launcher_start':
16
+ case 'requirement_create':
15
17
  case 'schedule_create':
16
18
  case 'schedule_pause':
17
19
  case 'schedule_resume':
20
+ case 'schedule_delete':
18
21
  case 'schedule_trigger':
19
22
  return 'workspace_write';
20
23
  case 'controller_get_owner':
@@ -26,6 +29,7 @@ function expectedActionRisk(action: SuggestedNextAction): SuggestedNextAction['r
26
29
  case 'plan_list':
27
30
  return 'readonly';
28
31
  case 'plan_approve':
32
+ case 'plan_accept_step':
29
33
  case 'plan_supersede':
30
34
  return 'workspace_write';
31
35
  case 'finalize':
@@ -40,7 +44,7 @@ const ALLOWED_FACADE_OPERATIONS: Record<FacadeTool, readonly string[]> = {
40
44
  rh_status: ['list', 'get', 'repair'],
41
45
  rh_inbox: ['list', 'get', 'ack', 'accept', 'resolve', 'dismiss', 'create'],
42
46
  rh_context: ['list', 'get', 'search'],
43
- rh_work: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'],
47
+ rh_work: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_disposition', 'controller_get_owner', 'launcher_start', 'requirement_create', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_accept_step', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_delete', 'schedule_trigger'],
44
48
  };
45
49
 
46
50
  export interface SuggestedActionValidationOptions {