@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
@@ -1,14 +1,19 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import {
3
3
  createHandoffItem,
4
+ listHandoffItems,
4
5
  type HandoffInboxStoreOptions,
5
6
  } from './handoff-inbox-store';
7
+ import { getControllerSession } from './controller-session-store';
6
8
  import {
7
9
  appendVerificationRecord,
8
10
  appendWorkEvidence,
9
11
  appendWorkHandoffRef,
10
12
  createWorkContract,
11
13
  getWorkContract,
14
+ listWorkContracts,
15
+ recordWorkCompletionReceipt,
16
+ recordWorkScopeEvidence,
12
17
  summarizeWorkContract,
13
18
  transitionWorkContractPhase,
14
19
  updateWorkContract,
@@ -17,8 +22,11 @@ import {
17
22
  import {
18
23
  claimPlanStepForWork,
19
24
  completePlanStepForWork,
25
+ getPlanContract,
20
26
  type PlanContractStoreOptions,
21
27
  } from './plan-contract-store';
28
+ import { withPrimaryWorkAdmissionLock } from './semantic-admission';
29
+ import { readRequirement } from '../persistence/requirement-store';
22
30
  import {
23
31
  classifyVerificationOutcome,
24
32
  normalizeCheckIds,
@@ -39,6 +47,7 @@ import type {
39
47
  SuggestedNextAction,
40
48
  VerificationRecord,
41
49
  WorkContract,
50
+ WorkKind,
42
51
  WorkRisk,
43
52
  } from './types';
44
53
  import { selectExecutionMode } from './types';
@@ -56,18 +65,30 @@ export interface GoalWorkloopContext {
56
65
  principalId?: string;
57
66
  controllerInstanceId?: string;
58
67
  workspaceFingerprint?: string;
68
+ /** Current net repository paths changed from the Work base plus dirty checkout paths. */
69
+ workspaceChangedPaths?: readonly string[];
59
70
  now?: () => string;
71
+ /** True only while a Gateway caller holds the cross-process primary Work admission lock. */
72
+ semanticAdmissionLocked?: boolean;
73
+ materializeIsolatedWorkspace?: (input: { workId: string; title: string; baseRef?: string; needsDependencies?: boolean }) => { checkoutId: string; root: string; baseRevision?: string | null; managed: true };
60
74
  }
61
75
 
76
+ export type WorkAdmissionRelation = 'continue' | 'extend' | 'parallel' | 'new_goal';
77
+
62
78
  export interface GoalWorkloopStartInput {
63
79
  objective: string;
64
80
  acceptanceCriteria?: string[];
65
81
  allowedPaths?: string[];
82
+ /** Non-authoritative first-pass discovery candidates. */
83
+ initialLikelyPaths?: string[];
66
84
  forbiddenPaths?: string[];
67
85
  checks?: string[];
68
86
  constraints?: WorkContract['constraints'];
69
87
  modeInput: ExecutionModeSelectionInput;
70
88
  requestedBy?: WorkContract['requestedBy'];
89
+ relatedWorkId?: string;
90
+ workRelation?: WorkAdmissionRelation;
91
+ requirementId?: string;
71
92
  taskId?: string;
72
93
  issueId?: string;
73
94
  approvalConfirmed?: boolean;
@@ -75,16 +96,35 @@ export interface GoalWorkloopStartInput {
75
96
  forceMode?: WorkContract['mode'];
76
97
  planId?: string;
77
98
  planStepId?: string;
99
+ /** Explicit technical evidence shape chosen by the semantic Controller. Never inferred from objective/check text. */
100
+ workKind?: Extract<WorkKind, 'repository_change' | 'completed_no_change' | 'investigation' | 'local_effect' | 'remote_effect' | 'reconciliation'>;
78
101
  }
79
102
 
80
103
  export interface GoalWorkloopContinueInput {
81
104
  workId: string;
82
105
  note?: string;
106
+ allowedPaths?: string[];
107
+ forbiddenPaths?: string[];
108
+ checks?: string[];
109
+ additionalLikelyPaths?: string[];
110
+ inspectedPaths?: string[];
111
+ /** Explicit Controller decision after deterministic acceptance failure; never inferred from note text. */
112
+ acceptanceFailureDecision?: 'repair' | 'rescope';
83
113
  }
84
114
 
85
115
  export interface GoalWorkloopVerifyInput {
86
116
  workId: string;
87
117
  checkId: string;
118
+ /** Exact Git revision observed by the authoritative Work-bound verification Process. */
119
+ sourceRevision?: string;
120
+ /** Exact dirty-workspace content identity observed by the authoritative check. */
121
+ workspaceFingerprint?: string;
122
+ /** Stable semantic check-input identity used for exact evidence reuse. */
123
+ verificationInputFingerprint?: string;
124
+ /** Bounded command/invocation audit identity; not part of reusable semantic inputs. */
125
+ commandFingerprint?: string;
126
+ /** Persistence-safe Process receipt. Accepted only when its Work/repo/check identity matches this verification. */
127
+ receipt?: VerificationRecord['receipt'];
88
128
  /** When true, simulate infrastructure failure rather than acceptance fail. */
89
129
  infrastructureFailed?: boolean;
90
130
  /** When true and check is valid, record acceptance failure. */
@@ -133,13 +173,13 @@ function workRiskFor(input: GoalWorkloopStartInput): WorkRisk {
133
173
  return 'medium';
134
174
  }
135
175
 
136
- function suggestedForWork(work: WorkContract, extras: SuggestedNextAction[] = []): SuggestedNextAction[] {
176
+ function suggestedForWorkIdentity(workId: string, checks: string[], extras: SuggestedNextAction[] = []): SuggestedNextAction[] {
137
177
  const base: SuggestedNextAction[] = [
138
178
  {
139
179
  label: 'Continue workloop',
140
180
  tool: 'rh_work',
141
181
  operation: 'continue',
142
- payload: { work_id: work.workId },
182
+ payload: { work_id: workId },
143
183
  risk: 'readonly',
144
184
  confidence: 'high',
145
185
  },
@@ -147,24 +187,28 @@ function suggestedForWork(work: WorkContract, extras: SuggestedNextAction[] = []
147
187
  label: 'Verify registered checks',
148
188
  tool: 'rh_work',
149
189
  operation: 'verify',
150
- payload: { work_id: work.workId, check_id: work.checks[0] },
190
+ payload: { work_id: workId, check_id: checks[0] },
151
191
  risk: 'workspace_write',
152
- confidence: work.checks[0] ? 'high' : 'low',
192
+ confidence: checks[0] ? 'high' : 'low',
153
193
  },
154
194
  {
155
195
  label: 'Finalize when ready',
156
196
  tool: 'rh_work',
157
197
  operation: 'finalize',
158
- payload: { work_id: work.workId },
198
+ payload: { work_id: workId },
159
199
  risk: 'readonly',
160
200
  confidence: 'medium',
161
201
  },
162
202
  ];
163
203
  return validateSuggestedNextActions([...extras, ...base], {
164
- validCheckIds: work.checks,
204
+ validCheckIds: checks,
165
205
  }).actions;
166
206
  }
167
207
 
208
+ function suggestedForWork(work: WorkContract, extras: SuggestedNextAction[] = []): SuggestedNextAction[] {
209
+ return suggestedForWorkIdentity(work.workId, work.checks, extras);
210
+ }
211
+
168
212
  function initialEvidence(objective: string): EvidenceRef {
169
213
  return {
170
214
  title: 'work contract created',
@@ -183,14 +227,79 @@ interface WorkCompletionEvidenceEvaluation {
183
227
  reasons: string[];
184
228
  }
185
229
 
230
+ function verificationRecordAppliesToCurrentWorkspace(
231
+ record: VerificationRecord,
232
+ currentRevision?: string,
233
+ currentWorkspaceFingerprint?: string,
234
+ ): boolean {
235
+ if (currentRevision && record.sourceRevision && record.sourceRevision !== currentRevision) return false;
236
+ if (
237
+ currentWorkspaceFingerprint
238
+ && record.workspaceFingerprint
239
+ && record.workspaceFingerprint !== currentWorkspaceFingerprint
240
+ ) return false;
241
+ return true;
242
+ }
243
+
244
+ function isAuthoritativeCurrentWorkVerification(
245
+ work: WorkContract,
246
+ record: VerificationRecord,
247
+ currentRevision?: string,
248
+ ): boolean {
249
+ const receipt = record.receipt;
250
+ return Boolean(
251
+ currentRevision
252
+ && record.outcome === 'valid_pass'
253
+ && record.sourceRevision === currentRevision
254
+ && receipt
255
+ && receipt.repoId === work.repoId
256
+ && receipt.workId === work.workId
257
+ && receipt.checkId === record.checkId
258
+ && receipt.ok === true
259
+ && receipt.timedOut === false
260
+ && receipt.cancelled === false,
261
+ );
262
+ }
263
+
264
+ interface WorkImplementationEvidenceEvaluation {
265
+ status: 'complete' | 'incomplete';
266
+ changedPaths: string[];
267
+ reasons: string[];
268
+ }
269
+
270
+ function evaluateWorkImplementationEvidence(
271
+ work: WorkContract,
272
+ currentWorkspaceChangedPaths: readonly string[] = [],
273
+ ): WorkImplementationEvidenceEvaluation {
274
+ if (work.workKind !== 'repository_change') {
275
+ return { status: 'complete', changedPaths: [], reasons: [] };
276
+ }
277
+
278
+ const changedPaths = [...new Set(currentWorkspaceChangedPaths.map((path) => path.trim()).filter(Boolean))].sort();
279
+ if (changedPaths.length > 0) {
280
+ return { status: 'complete', changedPaths, reasons: [] };
281
+ }
282
+
283
+ return {
284
+ status: 'incomplete',
285
+ changedPaths: [],
286
+ reasons: ['Repository-change Work has no current net source changes relative to its base revision. Verification evidence cannot substitute for implementation evidence.'],
287
+ };
288
+ }
289
+
186
290
  function evaluateWorkCompletionEvidence(
187
291
  work: WorkContract,
188
- history: ReconciledVerificationHistory = reconcileVerificationHistory(
189
- work.checkRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
190
- ),
292
+ currentRevision?: string,
293
+ currentWorkspaceFingerprint?: string,
191
294
  ): WorkCompletionEvidenceEvaluation {
295
+ const applicableCheckRefs = work.checkRefs.filter((record) =>
296
+ verificationRecordAppliesToCurrentWorkspace(record, currentRevision, currentWorkspaceFingerprint));
297
+ const history = reconcileVerificationHistory(
298
+ applicableCheckRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
299
+ );
192
300
  const missingChecks = work.checks.filter((checkId) => !history.validPasses.includes(checkId));
193
- const durableResultEvidence = work.evidenceRefs.some((evidence) => Boolean(evidence.evidenceId || evidence.artifactId));
301
+ const durableResultEvidence = work.evidenceRefs.some((evidence) => Boolean(evidence.evidenceId || evidence.artifactId))
302
+ || applicableCheckRefs.some((record) => isAuthoritativeCurrentWorkVerification(work, record, currentRevision));
194
303
  const reasons: string[] = [];
195
304
 
196
305
  if (history.acceptanceFailures.length > 0) {
@@ -207,7 +316,12 @@ function evaluateWorkCompletionEvidence(
207
316
  reasons.push(`Declared checks are missing valid_pass evidence: ${missingChecks.join(', ')}.`);
208
317
  }
209
318
  if (work.checks.length === 0 && !durableResultEvidence) {
210
- reasons.push('No durable result evidence (evidenceId or artifactId) was recorded for this no-check WorkContract.');
319
+ const staleWorkVerification = work.checkRefs.some((record) =>
320
+ Boolean(record.receipt)
321
+ && !verificationRecordAppliesToCurrentWorkspace(record, currentRevision, currentWorkspaceFingerprint));
322
+ reasons.push(staleWorkVerification
323
+ ? 'Work-bound verification evidence is stale for the current source/workspace identity.'
324
+ : 'No durable result evidence (evidenceId, artifactId, or current Work-bound verification receipt) was recorded for this no-check WorkContract.');
211
325
  }
212
326
 
213
327
  const complete = history.infrastructureIssues.length === 0
@@ -244,6 +358,13 @@ export function routeWorkStart(
244
358
  ? false
245
359
  : input.modeInput.requiresUserApproval === true || strategyConflictRequiresApproval,
246
360
  };
361
+ if (effectiveModeInput.explicitMode === 'scale' && (!input.planId || !input.planStepId || !ctx.planStore)) {
362
+ return buildFacadeResult({
363
+ status: 'blocked',
364
+ summary: 'SCALE_PLAN_REQUIRED: explicit Scale execution requires a bound approved PlanContract step.',
365
+ data: { executionStarted: false, workContractCreated: false, planRequired: true, explicitMode: 'scale' },
366
+ });
367
+ }
247
368
  const applyForcedMode = (selected: ReturnType<typeof selectExecutionMode>) => input.forceMode
248
369
  ? {
249
370
  ...selected,
@@ -280,11 +401,33 @@ export function routeWorkStart(
280
401
  selectedMode = selectExecutionMode({ ...effectiveModeInput, approvalConfirmed: true });
281
402
  policy = evaluateAccessPolicy(selectedMode);
282
403
  }
283
- const mode = applyForcedMode(selectedMode);
404
+ let mode = applyForcedMode(selectedMode);
405
+
406
+ // Route Policy is the sole execution-depth authority. Existing Work is
407
+ // ownership context, not a reason to upgrade an otherwise Direct operation
408
+ // into Durable Work. Explicit Work/Plan ownership is resolved only after a
409
+ // durable route has already been selected; checkout writer conflicts affect
410
+ // placement, not semantic task identity.
284
411
 
285
412
  if (mode.mode === 'direct_control') {
286
413
  const available = ctx.availableChecks ?? [];
287
414
  const normalized = normalizeCheckIds(input.checks ?? [], available);
415
+ // Explicit ownership may annotate a Direct operation, but it never changes
416
+ // the RouteDecision. The actual repository mutation can pass this Work id
417
+ // to repository_safe_patch_apply / repository_command_execute for evidence
418
+ // attribution while staying on the Direct/Process path.
419
+ const directOwnershipCandidates = input.relatedWorkId || (input.planId && input.planStepId)
420
+ ? listWorkContracts({ ...ctx.workStore, status: 'active', limit: 100 })
421
+ .filter((candidate) => (candidate.lifecycleRole ?? 'primary') === 'primary')
422
+ : [];
423
+ const directPlan = input.planId && ctx.planStore ? getPlanContract(ctx.planStore, input.planId) : undefined;
424
+ const directPlanStep = directPlan && input.planStepId ? directPlan.steps.find((step) => step.id === input.planStepId) : undefined;
425
+ const directOwner = (input.relatedWorkId
426
+ ? directOwnershipCandidates.find((candidate) => candidate.workId === input.relatedWorkId)
427
+ : undefined)
428
+ ?? (directPlanStep?.workId
429
+ ? directOwnershipCandidates.find((candidate) => candidate.workId === directPlanStep.workId)
430
+ : undefined);
288
431
  const suggested = validateSuggestedNextActions([
289
432
  {
290
433
  label: 'Apply bounded direct edit',
@@ -315,6 +458,13 @@ export function routeWorkStart(
315
458
  directControlPreserved: true,
316
459
  objective: input.objective.slice(0, 1_000),
317
460
  normalizedChecks: normalized,
461
+ ...(directOwner ? {
462
+ ownership: {
463
+ workId: directOwner.workId,
464
+ relation: input.workRelation ?? 'continue',
465
+ executionDepthPreserved: true,
466
+ },
467
+ } : {}),
318
468
  },
319
469
  warnings: [...policy.warnings, ...normalized.warnings],
320
470
  suggestedNextActions: suggested,
@@ -435,66 +585,332 @@ export function startGoalWorkloop(
435
585
  executionMode: 'direct_control' | 'goal_workloop' = 'goal_workloop',
436
586
  routeDecision = selectExecutionMode({ ...input.modeInput, objective: input.objective, knownPaths: input.allowedPaths }).routeDecision,
437
587
  ): FacadeResult {
588
+ const hasStrongAdmissionBinding = Boolean(input.planId || input.planStepId || input.requirementId || input.relatedWorkId || input.workRelation);
589
+ if (hasStrongAdmissionBinding && !ctx.semanticAdmissionLocked) {
590
+ return withPrimaryWorkAdmissionLock(ctx.workStore, () => startGoalWorkloop(
591
+ { ...ctx, semanticAdmissionLocked: true },
592
+ input,
593
+ policy,
594
+ executionMode,
595
+ routeDecision,
596
+ ));
597
+ }
438
598
  const at = nowIso(ctx);
439
599
  const available = ctx.availableChecks ?? [];
440
- const normalized = normalizeCheckIds(input.checks ?? [], available);
441
600
  const workspaceMode = input.constraints?.workspaceMode ?? 'auto';
601
+ const activeWorks = listWorkContracts({ ...ctx.workStore, status: 'active', limit: 100 })
602
+ .filter((candidate) => (candidate.lifecycleRole ?? 'primary') === 'primary');
603
+ const workspaceOwner = ctx.checkoutId
604
+ ? activeWorks.find((candidate) => candidate.checkoutId === ctx.checkoutId
605
+ && candidate.worktreePolicy.required !== true
606
+ && !candidate.worktreeRef)
607
+ : undefined;
608
+ const explicitRelatedWork = input.relatedWorkId
609
+ ? activeWorks.find((candidate) => candidate.workId === input.relatedWorkId)
610
+ : undefined;
611
+ const plan = input.planId && ctx.planStore ? getPlanContract(ctx.planStore, input.planId) : undefined;
612
+ const planStep = plan && input.planStepId ? plan.steps.find((step) => step.id === input.planStepId) : undefined;
613
+ const requestedRequirementId = input.requirementId?.trim() || undefined;
614
+ if (plan && requestedRequirementId && requestedRequirementId !== plan.requirementId) {
615
+ return buildFacadeResult({
616
+ status: 'blocked',
617
+ summary: `PLAN_REQUIREMENT_MISMATCH: ${plan.planId} is bound to ${plan.requirementId ?? 'no Requirement'}, not ${requestedRequirementId}. Use the Plan relationship authority or replan explicitly.`,
618
+ data: { executionStarted: false, workContractCreated: false, planId: plan.planId, planRequirementId: plan.requirementId, requestedRequirementId },
619
+ });
620
+ }
621
+ const effectiveRequirementId = requestedRequirementId || plan?.requirementId;
622
+ if (
623
+ effectiveRequirementId
624
+ && ctx.workStore.controllerHome
625
+ && !readRequirement({ controllerHome: ctx.workStore.controllerHome }, effectiveRequirementId)
626
+ ) {
627
+ return buildFacadeResult({
628
+ status: 'blocked',
629
+ summary: `${plan ? 'PLAN' : 'WORK'}_REQUIREMENT_NOT_FOUND: ${effectiveRequirementId}. Create or reconcile the Requirement authority before admitting Work.`,
630
+ data: { executionStarted: false, workContractCreated: false, requirementId: effectiveRequirementId, planId: plan?.planId },
631
+ });
632
+ }
633
+ const sameStringSet = (provided: string[] | undefined, frozen: string[]): boolean => {
634
+ if (provided === undefined) return true;
635
+ const actual = [...new Set(provided)].sort();
636
+ const expected = [...new Set(frozen)].sort();
637
+ return actual.length === expected.length && actual.every((value, index) => value === expected[index]);
638
+ };
639
+ if (planStep) {
640
+ const mismatches = [
641
+ !sameStringSet(input.acceptanceCriteria, planStep.acceptanceCriteria) && 'acceptance_criteria',
642
+ !sameStringSet(input.allowedPaths, planStep.allowedPaths) && 'allowed_paths',
643
+ !sameStringSet(input.forbiddenPaths, planStep.forbiddenPaths) && 'forbidden_paths',
644
+ !sameStringSet(input.checks, planStep.checks) && 'check_ids',
645
+ ].filter((value): value is string => Boolean(value));
646
+ if (mismatches.length > 0) {
647
+ return buildFacadeResult({
648
+ status: 'blocked',
649
+ summary: `PLAN_STEP_WORK_CONTRACT_MISMATCH: ${input.planId}/${input.planStepId} conflicts with frozen Plan step field(s): ${mismatches.join(', ')}. Replan instead of widening or narrowing Work admission.`,
650
+ data: { executionStarted: false, workContractCreated: false, planId: input.planId, planStepId: input.planStepId, mismatches },
651
+ });
652
+ }
653
+ }
654
+ const effectiveAcceptanceCriteria = planStep?.acceptanceCriteria ?? input.acceptanceCriteria ?? [];
655
+ const effectiveAllowedPaths = planStep?.allowedPaths ?? input.allowedPaths ?? [];
656
+ const effectiveForbiddenPaths = planStep?.forbiddenPaths ?? input.forbiddenPaths ?? [];
657
+ const effectiveChecks = planStep?.checks ?? input.checks ?? [];
658
+ const normalized = normalizeCheckIds(effectiveChecks, available);
659
+ const boundPlanStepWorks = input.planId && input.planStepId
660
+ ? activeWorks.filter((candidate) => candidate.planId === input.planId && candidate.planStepId === input.planStepId)
661
+ : [];
662
+ const explicitPlanStepWork = planStep?.workId ? activeWorks.find((candidate) => candidate.workId === planStep.workId) : undefined;
663
+ const planStepWork = explicitPlanStepWork ?? (boundPlanStepWorks.length === 1 ? boundPlanStepWorks[0] : undefined);
664
+ const requirementWorks = effectiveRequirementId
665
+ ? activeWorks.filter((candidate) => candidate.requirementId === effectiveRequirementId)
666
+ : [];
667
+ const deterministicTarget = input.relatedWorkId
668
+ ? explicitRelatedWork
669
+ : planStepWork ?? (requirementWorks.length === 1 ? requirementWorks[0] : undefined);
670
+ const requestedRelation = input.workRelation ?? (input.modeInput.requiresParallelism === true ? 'parallel' : undefined);
671
+ // Only strong semantic bindings participate in ownership resolution. An
672
+ // unrelated active Work or a checkout writer is a placement fact, not a
673
+ // semantic candidate for continue/extend/parallel/new_goal.
674
+ const candidateWorks = [...new Map(
675
+ [explicitRelatedWork, planStepWork, ...boundPlanStepWorks, ...requirementWorks]
676
+ .filter((candidate): candidate is WorkContract => Boolean(candidate))
677
+ .map((candidate) => [candidate.workId, candidate]),
678
+ ).values()].slice(0, 8);
679
+
680
+ const resolutionRequired = (reason: string, target = deterministicTarget): FacadeResult => buildFacadeResult({
681
+ status: 'ok',
682
+ summary: reason,
683
+ data: {
684
+ executionStarted: false,
685
+ workContractCreated: false,
686
+ admissionDecision: 'resolution_required',
687
+ resolutionRequired: true,
688
+ requestedBy: input.requestedBy ?? 'chatgpt',
689
+ ...(target ? { recommendedWork: summarizeWorkContract(target) } : {}),
690
+ candidates: candidateWorks.map(summarizeWorkContract),
691
+ allowedRelations: ['continue', 'extend', 'parallel', 'new_goal'],
692
+ },
693
+ evidenceRefs: target?.evidenceRefs ?? [],
694
+ suggestedNextActions: target
695
+ ? [{
696
+ label: 'Continue existing Work',
697
+ tool: 'rh_work',
698
+ operation: 'continue',
699
+ payload: { work_id: target.workId },
700
+ risk: 'readonly',
701
+ confidence: planStepWork || explicitRelatedWork ? 'high' : 'medium',
702
+ reason: 'Resolve intent before execution; do not create a second Work until the relationship is explicit.',
703
+ }]
704
+ : [],
705
+ rawAvailable: false,
706
+ });
707
+
708
+ if (boundPlanStepWorks.length > 1) {
709
+ return resolutionRequired(`PLAN_STEP_MULTIPLE_PRIMARY_WORKS: ${input.planId}/${input.planStepId} has ${boundPlanStepWorks.length} active primary Work records and requires repair before execution.`);
710
+ }
711
+ if (planStep?.workId && !explicitPlanStepWork) {
712
+ return buildFacadeResult({
713
+ status: 'blocked',
714
+ summary: `PLAN_STEP_BOUND_WORK_MISSING: ${input.planId}/${input.planStepId} is bound to ${planStep.workId}, but that Work is not active. Repair the exact binding; do not create a replacement Work.`,
715
+ data: { executionStarted: false, workContractCreated: false, planId: input.planId, planStepId: input.planStepId, boundWorkId: planStep.workId, repairRequired: true },
716
+ suggestedNextActions: [{
717
+ label: 'Diagnose exact Plan step binding',
718
+ tool: 'rh_work',
719
+ operation: 'repair',
720
+ payload: { plan_id: input.planId, plan_step_id: input.planStepId, repair_operation: 'diagnose', dry_run: true },
721
+ risk: 'readonly',
722
+ confidence: 'high',
723
+ reason: 'The Plan step has an exact Work identity but the active Work view cannot resolve it. Diagnose that binding before any replacement Work can be admitted.',
724
+ }],
725
+ rawAvailable: false,
726
+ });
727
+ }
728
+
729
+ if ((input.requestedBy ?? 'chatgpt') === 'scheduler') {
730
+ return resolutionRequired(
731
+ deterministicTarget
732
+ ? `SCHEDULER_CONTINUATION_REQUIRED: scheduler-triggered execution must continue its bound Work ${deterministicTarget.workId}; it must not create a new durable Work.`
733
+ : 'SCHEDULER_WORK_BINDING_REQUIRED: scheduler-triggered execution must bind and wake an existing durable Work; it must not invent a new Work from a prompt.',
734
+ );
735
+ }
736
+
737
+ if (planStepWork && !requestedRelation) {
738
+ return buildFacadeResult({
739
+ status: 'ok',
740
+ summary: `PLAN_STEP_REUSES_ACTIVE_WORK: ${input.planId}/${input.planStepId} already executes as ${planStepWork.workId}; reuse that Work instead of creating another.`,
741
+ data: {
742
+ executionStarted: false,
743
+ workContractCreated: false,
744
+ admissionDecision: 'reuse_existing',
745
+ resolutionRequired: false,
746
+ work: summarizeWorkContract(planStepWork),
747
+ },
748
+ evidenceRefs: planStepWork.evidenceRefs,
749
+ suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: planStepWork.workId }, risk: 'readonly', confidence: 'high' }],
750
+ rawAvailable: false,
751
+ });
752
+ }
753
+
754
+ if (requestedRelation === 'continue' || requestedRelation === 'extend') {
755
+ if (!deterministicTarget) {
756
+ return resolutionRequired(`${requestedRelation.toUpperCase()}_TARGET_REQUIRED: select related_work_id or bind the request to an active Plan/Requirement before execution.`);
757
+ }
758
+ if (requestedRelation === 'extend' && deterministicTarget.planId) {
759
+ return resolutionRequired(
760
+ `PLAN_EXTENSION_REQUIRES_REPLAN: Work ${deterministicTarget.workId} is governed by Plan ${deterministicTarget.planId}; record additional discovery in scopeEvidence, but update the authoritative Plan before widening policy or acceptance scope.`,
761
+ deterministicTarget,
762
+ );
763
+ }
764
+ const selected = requestedRelation === 'extend'
765
+ ? updateWorkContract(ctx.workStore, deterministicTarget.workId, {
766
+ acceptanceCriteria: [...new Set([...deterministicTarget.acceptanceCriteria, ...(input.acceptanceCriteria ?? [])])].slice(0, 30),
767
+ allowedPaths: [...new Set([...deterministicTarget.allowedPaths, ...(input.allowedPaths ?? [])])].slice(0, 50),
768
+ forbiddenPaths: [...new Set([...deterministicTarget.forbiddenPaths, ...(input.forbiddenPaths ?? [])])].slice(0, 50),
769
+ checks: [...new Set([...deterministicTarget.checks, ...normalized.validCheckIds])].slice(0, 30),
770
+ scopeEvidence: {
771
+ initialLikelyPaths: [...new Set([
772
+ ...(deterministicTarget.scopeEvidence?.initialLikelyPaths ?? deterministicTarget.allowedPaths),
773
+ ...(input.initialLikelyPaths ?? input.allowedPaths ?? []),
774
+ ])].slice(0, 100),
775
+ inspectedPaths: deterministicTarget.scopeEvidence?.inspectedPaths ?? [],
776
+ actualChangedPaths: deterministicTarget.scopeEvidence?.actualChangedPaths ?? [],
777
+ recordedAt: at,
778
+ },
779
+ scopeSummary: `Extended serially: ${input.objective}`.slice(0, 500),
780
+ continuationPrompt: `Continue work ${ctx.repoId}: ${deterministicTarget.objective.slice(0, 160)}. Additional requested scope: ${input.objective.slice(0, 500)}`,
781
+ })
782
+ : deterministicTarget;
783
+ return buildFacadeResult({
784
+ status: 'ok',
785
+ summary: `${requestedRelation === 'extend' ? 'Existing Work extended' : 'Existing Work selected'}: ${selected.workId}. No new WorkContract was created.`,
786
+ data: {
787
+ executionStarted: false,
788
+ workContractCreated: false,
789
+ admissionDecision: requestedRelation === 'extend' ? 'extend_existing' : 'reuse_existing',
790
+ resolutionRequired: false,
791
+ work: summarizeWorkContract(selected),
792
+ },
793
+ evidenceRefs: selected.evidenceRefs,
794
+ suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: selected.workId }, risk: 'readonly', confidence: 'high' }],
795
+ rawAvailable: false,
796
+ });
797
+ }
798
+
799
+ if (!requestedRelation && candidateWorks.length > 0) {
800
+ return resolutionRequired(
801
+ `WORK_ADMISSION_RESOLUTION_REQUIRED: ${candidateWorks.length} active Work candidate(s) exist. Classify this request as continue, extend, parallel, or new_goal before any durable Work is created.`,
802
+ );
803
+ }
804
+
805
+ const placementConflict = Boolean(workspaceOwner);
442
806
  const needsWorktree = executionMode === 'goal_workloop' && (input.constraints?.requireWorktree
443
807
  ?? (workspaceMode === 'isolated'
808
+ || placementConflict
809
+ || input.modeInput.requiresParallelism === true
444
810
  || (workspaceMode === 'auto' && routeDecision.requiresIsolation === true)));
811
+ if (!needsWorktree && workspaceOwner) {
812
+ return buildFacadeResult({
813
+ status: 'blocked',
814
+ summary: `WORKSPACE_OWNERSHIP_INVARIANT: unresolved admission reached checkout ${ctx.checkoutId} while active Work ${workspaceOwner.workId} owns it. This is an internal routing invariant, not a normal fallback path.`,
815
+ data: {
816
+ executionStarted: false,
817
+ workContractCreated: false,
818
+ conflictType: 'workspace_single_writer_invariant',
819
+ existingWork: summarizeWorkContract(workspaceOwner),
820
+ },
821
+ evidenceRefs: workspaceOwner.evidenceRefs,
822
+ rawAvailable: false,
823
+ });
824
+ }
445
825
  const generatedWorkId = workIdFor(input.objective);
446
826
  if (input.planId || input.planStepId) {
447
- if (!input.planId || !input.planStepId || !ctx.planStore || !ctx.sourceRevision) {
448
- return buildFacadeResult({ status: 'blocked', summary: 'PLAN_CONTEXT_REQUIRED: plan_id, plan_step_id and a current source revision are required.', data: { executionStarted: false } });
827
+ if (!input.planId || !input.planStepId || !ctx.planStore || !ctx.sourceRevision || !plan || !planStep) {
828
+ return buildFacadeResult({ status: 'blocked', summary: 'PLAN_CONTEXT_REQUIRED: plan_id, plan_step_id, an executable Plan step and a current source revision are required.', data: { executionStarted: false } });
449
829
  }
450
- let claimed: PlanContract;
451
- try {
452
- claimed = claimPlanStepForWork(ctx.planStore, {
830
+ if (plan.status !== 'approved' && plan.status !== 'executing') {
831
+ return buildFacadeResult({ status: 'blocked', summary: `PLAN_NOT_EXECUTABLE: ${plan.planId} is ${plan.status}`, data: { executionStarted: false, planId: plan.planId } });
832
+ }
833
+ if (plan.sourceRevision !== ctx.sourceRevision) {
834
+ const invalidated = claimPlanStepForWork(ctx.planStore, {
453
835
  planId: input.planId,
454
836
  stepId: input.planStepId,
455
837
  workId: generatedWorkId,
456
838
  sourceRevision: ctx.sourceRevision,
457
839
  });
458
- } catch (error) {
459
840
  return buildFacadeResult({
460
841
  status: 'blocked',
461
- summary: error instanceof Error ? error.message : 'PLAN_STEP_CLAIM_FAILED',
462
- data: { planId: input.planId, planStepId: input.planStepId, executionStarted: false },
842
+ summary: `PLAN_SOURCE_DRIFT: ${invalidated.planId} was invalidated because its source revision no longer matches. Replan before execution.`,
843
+ data: { planId: invalidated.planId, executionStarted: false, workContractCreated: false, replanRequired: true },
463
844
  });
464
845
  }
465
- if (claimed.status === 'invalidated_by_drift') {
466
- return buildFacadeResult({
467
- status: 'blocked',
468
- summary: `PLAN_SOURCE_DRIFT: ${claimed.planId} was invalidated because its source revision no longer matches. Replan before execution.`,
469
- data: { planId: claimed.planId, executionStarted: false, replanRequired: true },
470
- });
846
+ const unresolved = planStep.dependencies.filter((dependency) => plan.steps.find((candidate) => candidate.id === dependency)?.status !== 'completed');
847
+ if (unresolved.length > 0) {
848
+ return buildFacadeResult({ status: 'blocked', summary: `PLAN_STEP_DEPENDENCIES_PENDING: ${unresolved.join(', ')}`, data: { executionStarted: false, workContractCreated: false, planId: plan.planId, planStepId: planStep.id } });
849
+ }
850
+ if (planStep.status === 'completed') {
851
+ return buildFacadeResult({ status: 'blocked', summary: `PLAN_STEP_ALREADY_COMPLETED: ${planStep.id}`, data: { executionStarted: false, workContractCreated: false, planId: plan.planId, planStepId: planStep.id } });
471
852
  }
472
853
  }
854
+ const initialSuggestedNextActions = suggestedForWorkIdentity(
855
+ generatedWorkId,
856
+ normalized.validCheckIds,
857
+ normalized.suggestedNextActions,
858
+ );
859
+ // allowed_paths and initial_likely_paths are policy/discovery fences, not proof
860
+ // that this Work owns a repository mutation. External-effect Work therefore
861
+ // needs either an explicit repository_change semantic choice or concrete
862
+ // expected source-change evidence before Forge treats it as repository_change.
863
+ const repositoryChangeIntent = input.workKind === 'repository_change'
864
+ || (input.modeInput.expectedFiles ?? 0) > 0
865
+ || (input.modeInput.expectedChangedLines ?? 0) > 0;
866
+ const resolvedWorkKind = input.workKind ?? (
867
+ input.modeInput.requiresExternalEffect === true
868
+ && !repositoryChangeIntent
869
+ ? (input.modeInput.remoteWrite === true ? 'remote_effect' : 'local_effect')
870
+ : 'repository_change'
871
+ );
872
+ const remoteDeliveryRequired = resolvedWorkKind === 'repository_change'
873
+ && input.modeInput.requiresExternalEffect === true
874
+ && input.modeInput.remoteWrite === true;
473
875
  const work = createWorkContract(ctx.workStore, {
474
876
  workId: generatedWorkId,
475
877
  repoId: ctx.repoId,
476
- checkoutId: ctx.checkoutId,
878
+ checkoutId: needsWorktree ? undefined : ctx.checkoutId,
477
879
  principalId: ctx.principalId,
478
880
  controllerInstanceId: ctx.controllerInstanceId,
479
881
  baseRevision: ctx.sourceRevision,
480
- workspaceFingerprint: ctx.workspaceFingerprint,
882
+ workspaceFingerprint: needsWorktree ? undefined : ctx.workspaceFingerprint,
481
883
  routeDecisionFingerprint: routeDecision.inputFingerprint,
482
884
  routeDecision,
483
885
  mode: executionMode,
484
886
  objective: input.objective,
485
- acceptanceCriteria: input.acceptanceCriteria ?? [],
486
- constraints: input.constraints ?? { requireHandoffOnAmbiguity: true },
887
+ acceptanceCriteria: effectiveAcceptanceCriteria,
888
+ constraints: {
889
+ ...(input.constraints ?? { requireHandoffOnAmbiguity: true }),
890
+ ...(remoteDeliveryRequired ? { remoteDeliveryRequired: true } : {}),
891
+ },
487
892
  risk: workRiskFor(input),
893
+ // Remote delivery is a risk/effect dimension, not a reason to erase a
894
+ // repository-change Work's semantic identity. Pure external actions with no
895
+ // repository-change signal remain remote_effect and keep plugin receipt semantics.
896
+ workKind: resolvedWorkKind,
488
897
  status: 'running',
489
898
  phase: 'implementation',
490
899
  issueId: input.issueId,
491
900
  taskId: input.taskId,
901
+ requirementId: effectiveRequirementId,
492
902
  planId: input.planId,
493
903
  planStepId: input.planStepId,
494
904
  planSourceRevision: input.planId ? ctx.sourceRevision : undefined,
495
905
  scopeSummary: input.modeInput.scopeClear ? 'scope declared at start' : 'scope incomplete',
496
- allowedPaths: input.allowedPaths ?? [],
497
- forbiddenPaths: input.forbiddenPaths ?? [],
906
+ scopeEvidence: {
907
+ initialLikelyPaths: [...new Set(input.initialLikelyPaths ?? effectiveAllowedPaths)].slice(0, 100),
908
+ inspectedPaths: [],
909
+ actualChangedPaths: [],
910
+ recordedAt: at,
911
+ },
912
+ allowedPaths: effectiveAllowedPaths,
913
+ forbiddenPaths: effectiveForbiddenPaths,
498
914
  checks: normalized.validCheckIds,
499
915
  driver: {
500
916
  preferred: executionMode === 'direct_control'
@@ -509,6 +925,7 @@ export function startGoalWorkloop(
509
925
  ? 'Isolation was explicitly requested or required for parallel execution.'
510
926
  : 'Current workspace is the stability-first default; isolation remains opt-in.',
511
927
  },
928
+ worktreeRef: undefined,
512
929
  evidencePolicy: {
513
930
  defaultDetailLevel: 'summary',
514
931
  allowRawOptIn: true,
@@ -527,21 +944,39 @@ export function startGoalWorkloop(
527
944
  requestedBy: input.requestedBy ?? 'chatgpt',
528
945
  evidenceRefs: [initialEvidence(input.objective)],
529
946
  policyDecisions: policy ? [policy] : [],
530
- suggestedNextActions: [],
947
+ suggestedNextActions: initialSuggestedNextActions,
531
948
  continuationPrompt: `Continue work ${ctx.repoId}: ${input.objective.slice(0, 200)}`,
532
949
  });
533
950
 
534
- const suggested = suggestedForWork(work, normalized.suggestedNextActions);
535
- const updated = updateWorkContract(ctx.workStore, work.workId, {
536
- suggestedNextActions: suggested,
537
- updatedAt: at,
538
- });
951
+ if (input.planId && input.planStepId && ctx.planStore && ctx.sourceRevision) {
952
+ try {
953
+ const claimed = claimPlanStepForWork(ctx.planStore, {
954
+ planId: input.planId,
955
+ stepId: input.planStepId,
956
+ workId: work.workId,
957
+ sourceRevision: ctx.sourceRevision,
958
+ });
959
+ if (claimed.status === 'invalidated_by_drift') {
960
+ return buildFacadeResult({
961
+ status: 'blocked',
962
+ summary: `PLAN_SOURCE_DRIFT: ${claimed.planId} was invalidated because its source revision no longer matches. Repair or replan before execution.`,
963
+ data: { executionStarted: false, workContractCreated: true, work: summarizeWorkContract(work), planId: claimed.planId, canonicalWorkRetained: true, replanRequired: true },
964
+ });
965
+ }
966
+ } catch (error) {
967
+ return buildFacadeResult({
968
+ status: 'blocked',
969
+ summary: error instanceof Error ? error.message : 'PLAN_STEP_CLAIM_FAILED',
970
+ data: { planId: input.planId, planStepId: input.planStepId, executionStarted: false, workContractCreated: true, work: summarizeWorkContract(work), canonicalWorkRetained: true },
971
+ });
972
+ }
973
+ }
539
974
 
540
975
  return buildFacadeResult({
541
976
  status: 'ok',
542
977
  summary: executionMode === 'direct_control'
543
- ? `Direct-control Work lineage started as ${updated.workId}.`
544
- : `Goal workloop started as ${updated.workId}.`,
978
+ ? `Direct-control Work lineage started as ${work.workId}.`
979
+ : `Goal workloop started as ${work.workId}.`,
545
980
  data: {
546
981
  mode: {
547
982
  mode: executionMode,
@@ -555,20 +990,20 @@ export function startGoalWorkloop(
555
990
  routeDecision,
556
991
  },
557
992
  workContractCreated: true,
558
- work: summarizeWorkContract(updated),
559
- worktreeRequired: updated.worktreePolicy.required,
993
+ work: summarizeWorkContract(work),
994
+ worktreeRequired: work.worktreePolicy.required,
560
995
  normalizedChecks: normalized,
561
996
  policy,
562
997
  },
563
- evidenceRefs: updated.evidenceRefs,
998
+ evidenceRefs: work.evidenceRefs,
564
999
  warnings: normalized.warnings,
565
- suggestedNextActions: suggested,
1000
+ suggestedNextActions: initialSuggestedNextActions,
566
1001
  rawAvailable: false,
567
1002
  });
568
1003
  }
569
1004
 
570
1005
  export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopContinueInput): FacadeResult {
571
- const work = getWorkContract(ctx.workStore, input.workId);
1006
+ let work = getWorkContract(ctx.workStore, input.workId);
572
1007
  if (!work) {
573
1008
  return buildFacadeResult({
574
1009
  status: 'not_found',
@@ -595,78 +1030,223 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
595
1030
  });
596
1031
  }
597
1032
 
1033
+ const explicitPolicyScope = input.allowedPaths !== undefined
1034
+ || input.forbiddenPaths !== undefined
1035
+ || input.checks !== undefined;
1036
+ if (explicitPolicyScope) {
1037
+ if (work.planId) {
1038
+ return buildFacadeResult({
1039
+ status: 'blocked',
1040
+ summary: `PLAN_EXTENSION_REQUIRES_REPLAN: Work ${work.workId} is governed by Plan ${work.planId}; continue cannot widen frozen policy scope. Replan the authoritative Plan instead.`,
1041
+ data: { work: summarizeWorkContract(work), planId: work.planId, policyScopeUpdated: false },
1042
+ });
1043
+ }
1044
+ const normalizedChecks = normalizeCheckIds(input.checks ?? [], ctx.availableChecks ?? []);
1045
+ if (normalizedChecks.invalidCheckIds.length > 0) {
1046
+ return buildFacadeResult({
1047
+ status: 'blocked',
1048
+ summary: `WORK_CHECKS_INVALID: ${normalizedChecks.invalidCheckIds.join(', ')}. Work scope was not updated.`,
1049
+ data: { work: summarizeWorkContract(work), policyScopeUpdated: false, normalizedChecks },
1050
+ });
1051
+ }
1052
+ work = updateWorkContract(ctx.workStore, work.workId, {
1053
+ allowedPaths: [...new Set([...work.allowedPaths, ...(input.allowedPaths ?? [])])].slice(0, 50),
1054
+ forbiddenPaths: [...new Set([...work.forbiddenPaths, ...(input.forbiddenPaths ?? [])])].slice(0, 50),
1055
+ checks: [...new Set([...work.checks, ...normalizedChecks.validCheckIds])].slice(0, 30),
1056
+ scopeEvidence: {
1057
+ initialLikelyPaths: [...new Set([
1058
+ ...(work.scopeEvidence?.initialLikelyPaths ?? work.allowedPaths),
1059
+ ...(input.allowedPaths ?? []),
1060
+ ])].slice(0, 100),
1061
+ inspectedPaths: work.scopeEvidence?.inspectedPaths ?? [],
1062
+ actualChangedPaths: work.scopeEvidence?.actualChangedPaths ?? [],
1063
+ recordedAt: nowIso(ctx),
1064
+ },
1065
+ scopeSummary: `Continue adopted explicit policy scope for ${work.workId}.`.slice(0, 500),
1066
+ });
1067
+ }
1068
+
1069
+ if ((input.additionalLikelyPaths?.length ?? 0) > 0 || (input.inspectedPaths?.length ?? 0) > 0) {
1070
+ work = recordWorkScopeEvidence(ctx.workStore, work.workId, {
1071
+ initialLikelyPaths: input.additionalLikelyPaths,
1072
+ inspectedPaths: input.inspectedPaths,
1073
+ });
1074
+ }
1075
+
1076
+ const currentCheckRefs = work.checkRefs.filter((record) =>
1077
+ verificationRecordAppliesToCurrentWorkspace(record, ctx.sourceRevision, ctx.workspaceFingerprint));
598
1078
  const history = reconcileVerificationHistory(
599
- work.checkRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
1079
+ currentCheckRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
600
1080
  );
601
1081
 
602
- // Ambiguous: acceptance failure present → handoff for ChatGPT review rather than pretend progress.
603
- if (history.acceptanceFailures.length > 0 && work.recoveryPolicy.handoffOnAmbiguity) {
604
- const continuation = buildWorkContinuationSnapshot(work);
605
- const handoff = createHandoffItem(ctx.handoffStore, {
606
- id: handoffIdFor('continue'),
607
- repoId: ctx.repoId,
608
- workId: work.workId,
609
- title: 'Acceptance failure needs review',
610
- severity: 'needs_review',
611
- creationReason: 'ambiguous_outcome',
612
- reason: `Acceptance checks failed: ${history.acceptanceFailures.join(', ')}`,
613
- summary: 'Continue paused; ChatGPT must decide repair vs re-scope.',
614
- currentState: {
615
- repoId: ctx.repoId,
616
- workId: work.workId,
617
- mode: work.mode,
618
- statusSummary: 'waiting_for_review after acceptance failure',
619
- checks: history.acceptanceFailures.map((checkId) => ({ checkId, ok: false, outcome: 'valid_fail' as const })),
620
- workSemantics: continuation.semantics,
621
- reconciliationRequired: continuation.reconciliationRequired,
622
- nextSafeAction: continuation.nextSafeAction,
623
- },
624
- attemptedActions: ['continue'],
625
- evidenceRefs: work.evidenceRefs.slice(0, 5),
626
- blockingDecision: 'Decide whether to repair code, adjust acceptance criteria, or stop.',
627
- recommendedDecision: 'Inspect evidence and either repair or stop the workloop.',
628
- recommendedPrompt: work.continuationPrompt ?? `Continue from work ${work.workId}.`,
629
- recommendedContinuationPrompt: continuation.continuationPrompt,
630
- suggestedNextActions: [
631
- {
632
- label: 'Read work context',
633
- tool: 'rh_context',
634
- operation: 'get',
635
- payload: { work_id: work.workId },
636
- risk: 'readonly',
637
- },
638
- ],
1082
+ const explicitAcceptanceRepair = history.acceptanceFailures.length > 0
1083
+ && (input.acceptanceFailureDecision === 'repair' || input.acceptanceFailureDecision === 'rescope');
1084
+ if (explicitAcceptanceRepair) {
1085
+ const decision = input.acceptanceFailureDecision!;
1086
+ work = appendWorkEvidence(ctx.workStore, work.workId, {
1087
+ title: 'Controller acceptance-failure continuation decision',
1088
+ summary: `Controller explicitly chose bounded ${decision} after deterministic acceptance failure: ${history.acceptanceFailures.join(', ')}.`,
1089
+ detailLevel: 'summary',
639
1090
  });
640
1091
  transitionWorkContractPhase(ctx.workStore, work.workId, {
641
- status: 'ready',
642
- phase: 'verification',
643
- state: 'blocked',
644
- summary: `Acceptance failure requires review through handoff ${handoff.id}.`,
1092
+ status: 'running',
1093
+ phase: 'implementation',
1094
+ state: 'active',
1095
+ summary: `Controller chose bounded ${decision} after acceptance failure; implementation may resume before re-verification.`,
645
1096
  evidenceRefs: work.evidenceRefs,
646
1097
  });
647
- const updated = appendWorkHandoffRef(ctx.workStore, work.workId, handoff.id);
648
-
1098
+ work = getWorkContract(ctx.workStore, work.workId) ?? work;
649
1099
  return buildFacadeResult({
650
- status: 'blocked',
651
- summary: `Continue paused for ChatGPT review; handoff ${handoff.id} created. No background execution pretended.`,
1100
+ status: 'ok',
1101
+ summary: `Continue accepted explicit bounded ${decision} decision after acceptance failure; implementation may resume.`,
652
1102
  data: {
653
- work: summarizeWorkContract(updated),
654
- handoffId: handoff.id,
1103
+ work: summarizeWorkContract(work),
655
1104
  acceptanceFailures: history.acceptanceFailures,
656
1105
  infrastructureIssues: history.infrastructureIssues,
657
1106
  backgroundCompleted: false,
1107
+ nextStep: 'execute',
1108
+ acceptanceFailureDecision: decision,
658
1109
  },
659
1110
  evidenceRefs: work.evidenceRefs.slice(0, 5),
660
- suggestedNextActions: [
661
- {
1111
+ suggestedNextActions: suggestedForWork(work),
1112
+ });
1113
+ }
1114
+
1115
+ // Ambiguous acceptance failure without an explicit bounded Controller decision →
1116
+ // ask once per distinct failure evidence. Repeated continue calls must not
1117
+ // manufacture an unbounded handoff loop. A resolved handoff remains the decision
1118
+ // authority until newer valid-fail evidence; an unresolved matching handoff is reused.
1119
+ if (history.acceptanceFailures.length > 0 && !explicitAcceptanceRepair && work.recoveryPolicy.handoffOnAmbiguity) {
1120
+ const failureReason = `Acceptance checks failed: ${history.acceptanceFailures.join(', ')}`;
1121
+ const latestFailureAt = work.checkRefs
1122
+ .filter((record) => record.outcome === 'valid_fail' && history.acceptanceFailures.includes(record.checkId))
1123
+ .map((record) => record.recordedAt)
1124
+ .sort((left, right) => right.localeCompare(left))[0];
1125
+ const matchingHandoffs = listHandoffItems({ ...ctx.handoffStore, status: 'all', limit: 100 })
1126
+ .filter((item) => (
1127
+ item.workId === input.workId
1128
+ && item.creationReason === 'ambiguous_outcome'
1129
+ && item.title === 'Acceptance failure needs review'
1130
+ && item.reason === failureReason
1131
+ && (!latestFailureAt || item.createdAt >= latestFailureAt)
1132
+ ));
1133
+ const activeHandoff = matchingHandoffs.find((item) => item.status === 'pending' || item.status === 'acknowledged');
1134
+ if (activeHandoff) {
1135
+ return buildFacadeResult({
1136
+ status: 'blocked',
1137
+ summary: `Continue remains paused for ChatGPT review through existing handoff ${activeHandoff.id}.`,
1138
+ data: {
1139
+ work: summarizeWorkContract(work),
1140
+ handoffId: activeHandoff.id,
1141
+ acceptanceFailures: history.acceptanceFailures,
1142
+ infrastructureIssues: history.infrastructureIssues,
1143
+ backgroundCompleted: false,
1144
+ },
1145
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1146
+ suggestedNextActions: [{
662
1147
  label: 'Get handoff',
663
1148
  tool: 'rh_inbox',
664
1149
  operation: 'get',
665
- payload: { handoff_id: handoff.id },
1150
+ payload: { handoff_id: activeHandoff.id },
666
1151
  risk: 'readonly',
667
1152
  confidence: 'high',
1153
+ }],
1154
+ });
1155
+ }
1156
+
1157
+ const resolvedHandoff = matchingHandoffs.find((item) => item.status === 'resolved');
1158
+ if (!resolvedHandoff) {
1159
+ const continuation = buildWorkContinuationSnapshot(work);
1160
+ const handoff = createHandoffItem(ctx.handoffStore, {
1161
+ id: handoffIdFor('continue'),
1162
+ repoId: ctx.repoId,
1163
+ workId: work.workId,
1164
+ title: 'Acceptance failure needs review',
1165
+ severity: 'needs_review',
1166
+ creationReason: 'ambiguous_outcome',
1167
+ reason: failureReason,
1168
+ summary: 'Continue paused; ChatGPT must decide repair vs re-scope.',
1169
+ currentState: {
1170
+ repoId: ctx.repoId,
1171
+ workId: work.workId,
1172
+ mode: work.mode,
1173
+ statusSummary: 'waiting_for_review after acceptance failure',
1174
+ checks: history.acceptanceFailures.map((checkId) => ({ checkId, ok: false, outcome: 'valid_fail' as const })),
1175
+ workSemantics: continuation.semantics,
1176
+ reconciliationRequired: continuation.reconciliationRequired,
1177
+ nextSafeAction: continuation.nextSafeAction,
668
1178
  },
669
- ],
1179
+ attemptedActions: ['continue'],
1180
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1181
+ blockingDecision: 'Decide whether to repair code, adjust acceptance criteria, or stop.',
1182
+ recommendedDecision: 'Inspect evidence and either repair or stop the workloop.',
1183
+ recommendedPrompt: work.continuationPrompt ?? `Continue from work ${work.workId}.`,
1184
+ recommendedContinuationPrompt: continuation.continuationPrompt,
1185
+ suggestedNextActions: [
1186
+ {
1187
+ label: 'Read work context',
1188
+ tool: 'rh_context',
1189
+ operation: 'get',
1190
+ payload: { work_id: work.workId },
1191
+ risk: 'readonly',
1192
+ },
1193
+ ],
1194
+ });
1195
+ transitionWorkContractPhase(ctx.workStore, work.workId, {
1196
+ status: 'ready',
1197
+ phase: 'verification',
1198
+ state: 'blocked',
1199
+ summary: `Acceptance failure requires review through handoff ${handoff.id}.`,
1200
+ evidenceRefs: work.evidenceRefs,
1201
+ });
1202
+ const updated = appendWorkHandoffRef(ctx.workStore, work.workId, handoff.id);
1203
+
1204
+ return buildFacadeResult({
1205
+ status: 'blocked',
1206
+ summary: `Continue paused for ChatGPT review; handoff ${handoff.id} created. No background execution pretended.`,
1207
+ data: {
1208
+ work: summarizeWorkContract(updated),
1209
+ handoffId: handoff.id,
1210
+ acceptanceFailures: history.acceptanceFailures,
1211
+ infrastructureIssues: history.infrastructureIssues,
1212
+ backgroundCompleted: false,
1213
+ },
1214
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1215
+ suggestedNextActions: [
1216
+ {
1217
+ label: 'Get handoff',
1218
+ tool: 'rh_inbox',
1219
+ operation: 'get',
1220
+ payload: { handoff_id: handoff.id },
1221
+ risk: 'readonly',
1222
+ confidence: 'high',
1223
+ },
1224
+ ],
1225
+ });
1226
+ }
1227
+
1228
+ transitionWorkContractPhase(ctx.workStore, work.workId, {
1229
+ status: 'running',
1230
+ phase: 'verification',
1231
+ state: 'active',
1232
+ summary: `Resolved acceptance-failure handoff ${resolvedHandoff.id} authorizes bounded continuation to re-verification.`,
1233
+ evidenceRefs: work.evidenceRefs,
1234
+ });
1235
+ work = getWorkContract(ctx.workStore, work.workId) ?? work;
1236
+ return buildFacadeResult({
1237
+ status: 'ok',
1238
+ summary: `Continue accepted resolved acceptance-failure handoff ${resolvedHandoff.id}; re-verification may resume.`,
1239
+ data: {
1240
+ work: summarizeWorkContract(work),
1241
+ handoffId: resolvedHandoff.id,
1242
+ acceptanceFailures: history.acceptanceFailures,
1243
+ infrastructureIssues: history.infrastructureIssues,
1244
+ backgroundCompleted: false,
1245
+ nextStep: 'verify',
1246
+ remainingChecks: history.acceptanceFailures,
1247
+ },
1248
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1249
+ suggestedNextActions: suggestedForWork(work),
670
1250
  });
671
1251
  }
672
1252
 
@@ -708,6 +1288,47 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
708
1288
  });
709
1289
  }
710
1290
 
1291
+ const implementationEvidence = evaluateWorkImplementationEvidence(work, ctx.workspaceChangedPaths);
1292
+ if (implementationEvidence.status !== 'complete') {
1293
+ const suggested = validateSuggestedNextActions([
1294
+ {
1295
+ label: 'Implement the repository change before verification',
1296
+ tool: 'rh_context',
1297
+ operation: 'get',
1298
+ payload: { work_id: work.workId },
1299
+ risk: 'readonly',
1300
+ confidence: 'high',
1301
+ reason: 'Verification proves behavior only after the repository-change Work has current source changes.',
1302
+ },
1303
+ ]).actions;
1304
+ transitionWorkContractPhase(ctx.workStore, work.workId, {
1305
+ status: 'running',
1306
+ phase: 'implementation',
1307
+ state: 'active',
1308
+ summary: implementationEvidence.reasons.join(' '),
1309
+ evidenceRefs: work.evidenceRefs,
1310
+ });
1311
+ const updated = updateWorkContract(ctx.workStore, work.workId, { suggestedNextActions: suggested });
1312
+ return buildFacadeResult({
1313
+ status: 'blocked',
1314
+ summary: `Continue requires implementation before verification. ${implementationEvidence.reasons.join(' ')}`,
1315
+ data: {
1316
+ work: summarizeWorkContract(updated),
1317
+ backgroundCompleted: false,
1318
+ nextStep: 'execute',
1319
+ implementationEvidencePresent: false,
1320
+ workspaceChangedPaths: implementationEvidence.changedPaths,
1321
+ },
1322
+ suggestedNextActions: suggested,
1323
+ });
1324
+ }
1325
+
1326
+ if (implementationEvidence.changedPaths.length > 0) {
1327
+ recordWorkScopeEvidence(ctx.workStore, work.workId, {
1328
+ actualChangedPaths: implementationEvidence.changedPaths,
1329
+ });
1330
+ }
1331
+
711
1332
  if (work.checks.length > 0 && history.validPasses.length < work.checks.length) {
712
1333
  const remaining = work.checks.filter((checkId) => !history.validPasses.includes(checkId));
713
1334
  const suggested = validateSuggestedNextActions(
@@ -747,7 +1368,7 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
747
1368
  });
748
1369
  }
749
1370
 
750
- const completionEvidence = evaluateWorkCompletionEvidence(work, history);
1371
+ const completionEvidence = evaluateWorkCompletionEvidence(work, ctx.sourceRevision, ctx.workspaceFingerprint);
751
1372
  if (completionEvidence.status !== 'complete') {
752
1373
  const suggested = validateSuggestedNextActions([
753
1374
  {
@@ -839,16 +1460,60 @@ export function verifyGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloop
839
1460
  checkFailed: input.checkFailed,
840
1461
  skipped: input.skipped,
841
1462
  });
1463
+ if (work.status === 'completed' || work.status === 'cancelled' || work.status === 'failed') {
1464
+ const resolvedCheckId = classified.normalizedCheckId ?? classified.checkId;
1465
+ const existing = [...work.checkRefs].reverse().find((record) => record.checkId === resolvedCheckId);
1466
+ const completed = work.status === 'completed';
1467
+ return buildFacadeResult({
1468
+ status: completed ? 'ok' : 'blocked',
1469
+ summary: `WorkContract ${work.workId} is terminal (${work.status}); verification was not re-executed.`,
1470
+ data: {
1471
+ work: summarizeWorkContract(work),
1472
+ verification: {
1473
+ checkId: resolvedCheckId,
1474
+ ...(existing ? { outcome: existing.outcome } : {}),
1475
+ terminal: true,
1476
+ idempotent: true,
1477
+ reexecuted: false,
1478
+ isAcceptanceFailure: existing?.outcome === 'valid_fail',
1479
+ isInfrastructureIssue: existing?.outcome === 'invalid_check_id' || existing?.outcome === 'infrastructure_failure',
1480
+ doesNotRequestTaskChanges: existing?.outcome !== 'valid_fail',
1481
+ },
1482
+ backgroundCompleted: false,
1483
+ },
1484
+ evidenceRefs: existing?.evidenceRef ? [existing.evidenceRef] : [],
1485
+ warnings: classified.warnings,
1486
+ suggestedNextActions: [{ label: 'Inspect work via context', tool: 'rh_context', operation: 'get', payload: { work_id: work.workId }, risk: 'readonly', confidence: 'high' }],
1487
+ });
1488
+ }
842
1489
 
843
1490
  const at = nowIso(ctx);
1491
+ const resolvedCheckId = classified.normalizedCheckId ?? classified.checkId;
1492
+ const receipt = input.receipt
1493
+ && input.receipt.repoId === work.repoId
1494
+ && input.receipt.workId === work.workId
1495
+ && input.receipt.checkId === resolvedCheckId
1496
+ ? input.receipt
1497
+ : undefined;
1498
+ const sourceRevision = receipt && input.sourceRevision?.trim() ? input.sourceRevision.trim() : undefined;
1499
+ const verificationSummary = receipt
1500
+ ? `${classified.summary} Durable Process receipt ${receipt.receiptId}.`
1501
+ : classified.summary;
844
1502
  const record: VerificationRecord = {
845
- checkId: classified.normalizedCheckId ?? classified.checkId,
1503
+ checkId: resolvedCheckId,
846
1504
  outcome: classified.outcome,
847
- summary: classified.summary,
1505
+ summary: verificationSummary,
848
1506
  recordedAt: at,
1507
+ sourceRevision,
1508
+ workspaceFingerprint: receipt && sourceRevision ? input.workspaceFingerprint : undefined,
1509
+ verificationInputFingerprint: receipt && sourceRevision ? input.verificationInputFingerprint : undefined,
1510
+ commandFingerprint: receipt && sourceRevision ? input.commandFingerprint : undefined,
1511
+ startedAt: receipt?.startedAt,
1512
+ completedAt: receipt?.finishedAt,
1513
+ receipt,
849
1514
  evidenceRef: {
850
1515
  title: `verification:${classified.outcome}`,
851
- summary: classified.summary,
1516
+ summary: verificationSummary,
852
1517
  detailLevel: 'summary',
853
1518
  },
854
1519
  };
@@ -878,12 +1543,16 @@ export function verifyGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloop
878
1543
  ? 'failed'
879
1544
  : 'ok';
880
1545
 
1546
+ const completionEvidence = classified.outcome === 'valid_pass'
1547
+ ? evaluateWorkCompletionEvidence(updated, sourceRevision, input.workspaceFingerprint)
1548
+ : undefined;
1549
+ const validPassReadyToFinalize = completionEvidence?.status === 'complete';
881
1550
  const suggested = validateSuggestedNextActions(
882
1551
  classified.outcome === 'valid_pass'
883
1552
  ? [{
884
- label: 'Continue workloop',
1553
+ label: validPassReadyToFinalize ? 'Finalize work' : 'Continue workloop',
885
1554
  tool: 'rh_work',
886
- operation: 'continue',
1555
+ operation: validPassReadyToFinalize ? 'finalize' : 'continue',
887
1556
  payload: { work_id: work.workId },
888
1557
  risk: 'readonly',
889
1558
  confidence: 'high',
@@ -922,6 +1591,9 @@ export function verifyGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloop
922
1591
  doesNotRequestTaskChanges: !classified.isAcceptanceFailure,
923
1592
  },
924
1593
  backgroundCompleted: false,
1594
+ ...(classified.outcome === 'valid_pass'
1595
+ ? { nextStep: validPassReadyToFinalize ? 'finalize' : 'continue' }
1596
+ : {}),
925
1597
  },
926
1598
  warnings: classified.warnings,
927
1599
  evidenceRefs: record.evidenceRef ? [record.evidenceRef] : [],
@@ -970,7 +1642,7 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
970
1642
  });
971
1643
  }
972
1644
 
973
- const completionEvidence = evaluateWorkCompletionEvidence(work);
1645
+ const completionEvidence = evaluateWorkCompletionEvidence(work, ctx.sourceRevision, ctx.workspaceFingerprint);
974
1646
  const history = completionEvidence.history;
975
1647
 
976
1648
  if (input.forceFailed || completionEvidence.status === 'failed') {
@@ -1045,6 +1717,47 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1045
1717
  });
1046
1718
  }
1047
1719
 
1720
+ // For a controller-local effect Work, the semantic Controller's explicit
1721
+ // finalize call is the terminalization decision, but it is admissible only
1722
+ // after durable result evidence exists. Record one canonical Work receipt here
1723
+ // instead of routing an effect-only Work through Git delivery. Remote effects
1724
+ // remain bound to their durable plugin action receipt in the plugin layer.
1725
+ if (work.workKind === 'local_effect' && !work.completionReceipt && completionEvidence.durableResultEvidence) {
1726
+ const recordedAt = nowIso(ctx);
1727
+ const completed = recordWorkCompletionReceipt(
1728
+ ctx.workStore,
1729
+ work.workId,
1730
+ {
1731
+ schemaVersion: 1,
1732
+ receiptId: `LFX-WORK-${randomUUID()}`,
1733
+ source: 'local_effect',
1734
+ workId: work.workId,
1735
+ operation: 'controller_work/local_effect',
1736
+ target: { kind: 'controller_local', id: work.workId },
1737
+ changed: true,
1738
+ recordedAt,
1739
+ },
1740
+ 'completed_local',
1741
+ 'local_effect',
1742
+ );
1743
+ if (completed.planId && completed.planStepId && ctx.planStore) {
1744
+ completePlanStepForWork(ctx.planStore, { planId: completed.planId, stepId: completed.planStepId, work: completed });
1745
+ }
1746
+ return buildFacadeResult({
1747
+ status: 'ok',
1748
+ summary: `Finalize result: succeeded for ${work.workId}.`,
1749
+ data: {
1750
+ work: summarizeWorkContract(completed),
1751
+ finalStatus: 'completed',
1752
+ completionReceipt: completed.completionReceipt,
1753
+ validPasses: history.validPasses,
1754
+ hiddenFailure: false,
1755
+ },
1756
+ evidenceRefs: completed.evidenceRefs.slice(0, 5),
1757
+ suggestedNextActions: [{ label: 'Read controller status', tool: 'rh_status', operation: 'get', risk: 'readonly' }],
1758
+ });
1759
+ }
1760
+
1048
1761
  // Checks and result evidence are necessary but not sufficient. Delivery and
1049
1762
  // cleanup must produce the exact Work-owned receipt before Work can become
1050
1763
  // terminal; a Run exit or a missing receipt is never completion authority.
@@ -1099,6 +1812,38 @@ export function stopGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopSt
1099
1812
  });
1100
1813
  }
1101
1814
 
1815
+ const owner = ctx.workStore.controllerHome
1816
+ ? getControllerSession({ controllerHome: ctx.workStore.controllerHome, repoId: ctx.repoId }, work.workId)
1817
+ : undefined;
1818
+ if (owner) {
1819
+ const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
1820
+ const ownerInstanceId = owner.controllerInstanceId?.trim();
1821
+ const callerPrincipal = ctx.principalId?.trim();
1822
+ const callerInstanceId = ctx.controllerInstanceId?.trim();
1823
+ const sameAuthority = Boolean(
1824
+ callerPrincipal
1825
+ && ownerPrincipal === callerPrincipal
1826
+ && (!ownerInstanceId || ownerInstanceId === callerInstanceId),
1827
+ );
1828
+ if (!sameAuthority) {
1829
+ return buildFacadeResult({
1830
+ status: 'blocked',
1831
+ summary: `WORK_TERMINALIZATION_ACTIVE_CONTROLLER_FENCE: ${work.workId} has a newer active Controller claim generation ${owner.claimGeneration ?? 1}.`,
1832
+ data: {
1833
+ work: summarizeWorkContract(work),
1834
+ activeController: {
1835
+ controllerId: owner.controllerId,
1836
+ principalId: ownerPrincipal,
1837
+ controllerInstanceId: owner.controllerInstanceId,
1838
+ sessionId: owner.sessionId,
1839
+ claimGeneration: owner.claimGeneration ?? 1,
1840
+ },
1841
+ finalStatus: work.status,
1842
+ },
1843
+ });
1844
+ }
1845
+ }
1846
+
1102
1847
  const destructiveCleanup = input.authorizeDestructiveCleanup === true;
1103
1848
  transitionWorkContractPhase(ctx.workStore, work.workId, {
1104
1849
  status: 'cancelled',
@@ -1115,6 +1860,17 @@ export function stopGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopSt
1115
1860
  // cleanup handler verifies ownership, cleanliness and successful removal.
1116
1861
  worktreeRef: work.worktreeRef,
1117
1862
  });
1863
+ const planId = updated.planId;
1864
+ const planStepId = updated.planStepId;
1865
+ let plan = planId && ctx.planStore
1866
+ ? getPlanContract(ctx.planStore, planId)
1867
+ : undefined;
1868
+ if (plan && planId && planStepId && ctx.planStore) {
1869
+ const step = plan.steps.find((entry) => entry.id === planStepId);
1870
+ if (step?.workId === updated.workId) {
1871
+ plan = completePlanStepForWork(ctx.planStore, { planId, stepId: planStepId, work: updated });
1872
+ }
1873
+ }
1118
1874
 
1119
1875
  return buildFacadeResult({
1120
1876
  status: 'ok',
@@ -1126,6 +1882,7 @@ export function stopGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopSt
1126
1882
  worktreeDeleted: false,
1127
1883
  cleanupPending: destructiveCleanup && Boolean(work.worktreeRef),
1128
1884
  destructiveCleanupAuthorized: destructiveCleanup,
1885
+ ...(plan ? { plan } : {}),
1129
1886
  },
1130
1887
  evidenceRefs: work.evidenceRefs.slice(0, 5),
1131
1888
  suggestedNextActions: [
@@ -1150,6 +1907,7 @@ export function runGoalWorkloop(
1150
1907
  objective: String(args.objective ?? ''),
1151
1908
  acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : undefined,
1152
1909
  allowedPaths: Array.isArray(args.allowed_paths) ? args.allowed_paths.map(String) : undefined,
1910
+ initialLikelyPaths: Array.isArray(args.initial_likely_paths) ? args.initial_likely_paths.map(String) : undefined,
1153
1911
  forbiddenPaths: Array.isArray(args.forbidden_paths) ? args.forbidden_paths.map(String) : undefined,
1154
1912
  checks: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
1155
1913
  modeInput: {
@@ -1160,6 +1918,7 @@ export function runGoalWorkloop(
1160
1918
  requiresInvestigation: args.requires_investigation === true,
1161
1919
  requiresLongRunningChecks: args.requires_long_running_checks === true,
1162
1920
  requiresParallelism: args.requires_parallelism === true,
1921
+ explicitMode: args.mode === 'scale' ? 'scale' : undefined,
1163
1922
  needsDependencies: args.needs_dependencies === true,
1164
1923
  requiresRecovery: args.requires_recovery === true,
1165
1924
  requiresWorker: args.requires_worker === true,
@@ -1179,13 +1938,34 @@ export function runGoalWorkloop(
1179
1938
  forceMode: args.force_mode === 'direct_control' || args.force_mode === 'goal_workloop' || args.force_mode === 'handoff_only'
1180
1939
  ? args.force_mode
1181
1940
  : undefined,
1941
+ relatedWorkId: typeof args.related_work_id === 'string' ? args.related_work_id : undefined,
1942
+ workRelation: args.work_relation === 'continue' || args.work_relation === 'extend' || args.work_relation === 'parallel' || args.work_relation === 'new_goal'
1943
+ ? args.work_relation
1944
+ : undefined,
1945
+ requirementId: typeof args.requirement_id === 'string' ? args.requirement_id : undefined,
1182
1946
  planId: typeof args.plan_id === 'string' ? args.plan_id : undefined,
1183
1947
  planStepId: typeof args.plan_step_id === 'string' ? args.plan_step_id : undefined,
1948
+ workKind: args.work_kind === 'repository_change'
1949
+ || args.work_kind === 'completed_no_change'
1950
+ || args.work_kind === 'investigation'
1951
+ || args.work_kind === 'local_effect'
1952
+ || args.work_kind === 'remote_effect'
1953
+ || args.work_kind === 'reconciliation'
1954
+ ? args.work_kind
1955
+ : undefined,
1184
1956
  });
1185
1957
  case 'continue':
1186
1958
  return continueGoalWorkloop(ctx, {
1187
1959
  workId: String(args.work_id ?? ''),
1188
1960
  note: typeof args.note === 'string' ? args.note : undefined,
1961
+ allowedPaths: Array.isArray(args.allowed_paths) ? args.allowed_paths.map(String) : undefined,
1962
+ forbiddenPaths: Array.isArray(args.forbidden_paths) ? args.forbidden_paths.map(String) : undefined,
1963
+ checks: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
1964
+ additionalLikelyPaths: Array.isArray(args.additional_likely_paths) ? args.additional_likely_paths.map(String) : undefined,
1965
+ inspectedPaths: Array.isArray(args.inspected_paths) ? args.inspected_paths.map(String) : undefined,
1966
+ acceptanceFailureDecision: args.acceptance_failure_decision === 'repair' || args.acceptance_failure_decision === 'rescope'
1967
+ ? args.acceptance_failure_decision
1968
+ : undefined,
1189
1969
  });
1190
1970
  case 'verify':
1191
1971
  return verifyGoalWorkloop(ctx, {