@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,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
- import { existsSync, readFileSync } from 'fs';
2
+ import { existsSync, readFileSync, realpathSync } from 'fs';
3
3
  import { spawnSync } from 'child_process';
4
4
  import { basename, isAbsolute, relative, resolve } from 'path';
5
5
  import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
@@ -14,25 +14,26 @@ import {
14
14
  setRepositoryCheckoutLifecycle,
15
15
  } from '../../../cli/repositories/registry';
16
16
  import { withControllerLock } from '../../../cli/repositories/locks';
17
- import { repositoryGitCommit, repositoryGitDeleteBranch, repositoryGitFinishWorkflow, repositoryGitStatus, repositoryGitDiff } from '../../../cli/repositories/structured-git';
17
+ import { repositoryGitCommit, repositoryGitDeleteBranch, repositoryGitFinishWorkflow, repositoryGitMergeBranch, repositoryGitRebaseOnto, repositoryGitStatus, repositoryGitDiff } from '../../../cli/repositories/structured-git';
18
18
  import { previewRepositoryCommandExecution } from '../../../cli/repositories/command-executor';
19
19
  import { classifyRepositoryCommand } from '../../../cli/repositories/command-classifier';
20
20
  import { globMatches } from '../../../cli/mcp/paths';
21
21
  import { ensureManagedWorkspace } from '../../execution/managed-workspace';
22
- import { listControllerChecks } from '../../../cli/controller/check-runner';
22
+ import { listControllerChecks, type ControllerCheck } from '../../../cli/controller/check-runner';
23
23
  import { readRepositoryAccessPolicy } from '../../control-plane/governance/access-policy';
24
- import { appendWorkEvidence, createWorkContract, getWorkContract, recordWorkCompletionReceipt, updateWorkContract, appendVerificationRecord } from '../../control-plane/facade/work-contract-store';
24
+ import { appendWorkEvidence, createWorkContract, getWorkContract, recordWorkCompletionReceipt, transitionWorkContractPhase, updateWorkContract, appendVerificationRecord } from '../../control-plane/facade/work-contract-store';
25
25
  import { completeRequirementFromWork } from '../../control-plane/persistence/requirement-store';
26
- import { isTerminalWorkContractStatus, type WorkReconciliationRecord } from '../../control-plane/facade/types';
26
+ import { isTerminalWorkContractStatus, type VerificationRecord, type WorkReconciliationRecord } from '../../control-plane/facade/types';
27
27
  import { buildWorkContinuationSnapshot } from '../../control-plane/facade/work-continuation';
28
- import { claimControllerSession, getControllerSession, releaseControllerSession, resumeControllerSession } from '../../control-plane/facade/controller-session-store';
28
+ import { claimControllerSession, getControllerSession, releaseControllerSession, releaseControllerSessionWithAuthority, resumeControllerSession, withControllerSessionTerminalizationFence } from '../../control-plane/facade/controller-session-store';
29
29
  import { currentControllerInstanceId, requireExecutionSession, startExecutionSession, updateExecutionSession, type ExecutionSessionContext, type SessionIdentity } from '../../control-plane/execution/session-store';
30
30
  import { currentPermissionSnapshotVersion, validateWorkHandle } from '../../control-plane/execution/validation';
31
- import { commandFingerprint, verificationInputFingerprint, workspaceValidationFingerprint, workValidationInputFingerprint } from '../../control-plane/execution/verification-evidence';
31
+ import { commandFingerprint, effectiveVerificationEvidence, verificationInputFingerprint, workspaceValidationFingerprint, workValidationInputFingerprint } from '../../control-plane/execution/verification-evidence';
32
32
  import { assertExecutionIdentity, executionIdentityForWork, executionIdentityFromCoordinates, resolveLegacyWorkContractIdentity } from '../../control-plane/execution/execution-identity';
33
33
  import { withWorkPrepareRequest } from '../../control-plane/execution/work-prepare-request-store';
34
- import { markWorkHandleFailed, newWorkId, readWorkHandle, transitionWorkHandle, writeWorkHandle, type WorkFinalizationStages, type WorkHandleState, type WorkTerminalOutcome } from '../../control-plane/execution/work-handle-store';
34
+ import { markWorkHandleFailed, newWorkId, readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, writeWorkHandle, type WorkFinalizationStages, type WorkHandleState, type WorkTerminalOutcome } from '../../control-plane/execution/work-handle-store';
35
35
  import { cleanupTerminalWork } from '../../control-plane/execution/work-terminal-cleanup';
36
+ import { pushExactWorkRemoteDelivery, type WorkRemoteDeliveryReceipt } from '../../control-plane/execution/work-remote-delivery';
36
37
  import { assertResolvedAuthorization, createGoalDelegation, decideAuthorization, resolveAuthorizationRequest, type AuthorizationDecision, type AuthorizationRiskClass } from '../../control-plane/governance/authorization';
37
38
  import { readControllerResult, searchControllerResult, writeControllerResult } from '../../evidence/result-store';
38
39
  import { resumeExecutionJobAfterApproval } from '../../execution/jobs/store';
@@ -42,6 +43,7 @@ import { markRepositoryProjectionDirty } from '../../projections/invalidation';
42
43
  import { executeRepositoryCommandViaProcessRuntime } from '../../execution/process-runtime/command-facade';
43
44
  import { getCheckProcessHandle } from '../../execution/process-runtime/check-facade';
44
45
  import { processCheckCompletionReceipt } from '../../execution/process-runtime/check-receipt';
46
+ import { classifyPersistedCheckTerminalEvidence } from '../../execution/process-runtime/check-result';
45
47
  import { claimProcessInvocation, getProcessRecord } from '../../execution/process-runtime/store';
46
48
  import { runPersistedCheckViaProcessRuntime } from './persisted-check-process';
47
49
  import { hasCurrentWorkValidationAuthority, markWorkValidationPending, projectWorkValidationOutcome, reconcileWorkValidation } from './work-validation-reconciler';
@@ -62,7 +64,7 @@ export const executionToolDefinitions: McpToolDefinition[] = [
62
64
  definition('work_prepare', 'Prepare or reuse one controller-owned work handle and bind it to a WorkContract, checkout, branch, and permission snapshot.', {
63
65
  session_id: sessionId, repo_id: repoId, checkout_id: { type: 'string' }, work_id: workId,
64
66
  objective: { type: 'string' }, goal_id: { type: 'string' }, acceptance_criteria: { type: 'array', items: { type: 'string' } }, allowed_paths: { type: 'array', items: { type: 'string' } }, checks: { type: 'array', items: { type: 'string' } },
65
- isolation: { type: 'string', enum: ['reuse', 'new_worktree', 'auto'] }, base_ref: { type: 'string' },
67
+ isolation: { type: 'string', enum: ['reuse', 'new_worktree', 'auto'] }, base_ref: { type: 'string' }, needs_dependencies: { type: 'boolean' },
66
68
  expected_previous_head: { type: 'string', description: 'Explicit prior WorkHandle HEAD required for audited successor adoption of an existing work_id.' },
67
69
  adopt_candidate_head: { type: 'string', description: 'Explicit current successor commit to adopt after exact identity, ownership, cleanliness, ancestry, and path-scope validation.' },
68
70
  }, [], false),
@@ -76,7 +78,7 @@ export const executionToolDefinitions: McpToolDefinition[] = [
76
78
  session_id: sessionId, controller_id: { type: 'string', description: 'Controller identity that holds the Work lease. Defaults to the authenticated principal.' }, repo_id: repoId, work_id: workId, check_ids: { type: 'array', items: { type: 'string' } }, commands: { type: 'array', items: { type: 'object' } },
77
79
  }, ['work_id'], false),
78
80
  definition('work_finalize', 'Idempotently validate, commit, merge, clean a managed worktree, and complete the existing WorkContract in independently recorded stages.', {
79
- session_id: sessionId, controller_id: { type: 'string', description: 'Controller identity that holds the Work lease. Defaults to the authenticated principal.' }, repo_id: repoId, work_id: workId, commit: { type: 'boolean' }, message: { type: 'string' }, merge: { type: 'boolean' }, target_branch: { type: 'string' }, delete_branch: { type: 'boolean' }, cleanup: { type: 'boolean' }, no_ff: { type: 'boolean' }, approval_request_id: { type: 'string' },
81
+ session_id: sessionId, controller_id: { type: 'string', description: 'Controller identity that holds the Work lease. Defaults to the authenticated principal.' }, repo_id: repoId, work_id: workId, commit: { type: 'boolean' }, message: { type: 'string' }, merge: { type: 'boolean' }, target_branch: { type: 'string' }, remote_write: { type: 'boolean', description: 'When true, push the exact locally integrated target revision to origin before cleanup and Work terminalization.' }, delete_branch: { type: 'boolean' }, cleanup: { type: 'boolean' }, no_ff: { type: 'boolean' }, approval_request_id: { type: 'string' },
80
82
  completion_outcome: { type: 'string', enum: ['completed_changed', 'completed_no_change'] }, no_change_evidence: { type: 'string', description: 'Objective-specific proof that the requested state already holds; required for completed_no_change.' },
81
83
  }, ['work_id'], false, true),
82
84
  definition('approval_resolve', 'Resolve a controller approval request from the current conversation; GUI approval is optional and not required for continuation.', { session_id: sessionId, repo_id: repoId, work_id: workId, approval_request_id: { type: 'string' }, confirm_authorization: { type: 'boolean' } }, ['approval_request_id', 'confirm_authorization'], false),
@@ -158,11 +160,12 @@ function compactHandle(handle: WorkHandleState): Record<string, unknown> {
158
160
  return {
159
161
  workId: handle.workId, sessionId: handle.sessionId, repoId: handle.repositoryId, checkoutId: handle.checkoutId,
160
162
  worktreePath: handle.worktreePath, branch: handle.branch, sourceCheckoutId: handle.sourceCheckoutId, goalId: handle.goalId, delegationVersion: handle.delegationVersion,
161
- workContractId: handle.workContractId, baseCommit: handle.baseCommit, expectedHead: handle.expectedHead,
163
+ workContractId: handle.workContractId, baseCommit: handle.baseCommit, deliveryBaseCommit: handle.deliveryBaseCommit, expectedHead: handle.expectedHead,
162
164
  permissionSnapshotVersion: handle.permissionSnapshotVersion, state: handle.state, managedWorktree: handle.managedWorktree,
163
165
  createdAt: handle.createdAt, updatedAt: handle.updatedAt, finalization: handle.finalization,
164
166
  ...(handle.failureReason ? { failureReason: handle.failureReason } : {}),
165
167
  ...(handle.cleanupResponsibility ? { cleanupResponsibility: handle.cleanupResponsibility } : {}),
168
+ ...(handle.terminalResourceDisposition ? { terminalResourceDisposition: handle.terminalResourceDisposition } : {}),
166
169
  ...(handle.cleanupReceipt ? { cleanupReceipt: handle.cleanupReceipt } : {}),
167
170
  };
168
171
  }
@@ -171,9 +174,13 @@ function initialStage(): WorkFinalizationStages {
171
174
  return { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' };
172
175
  }
173
176
 
177
+ function gitRevision(root: string, revision: string): string | undefined {
178
+ const output = spawnSync('git', ['-C', root, 'rev-parse', '--verify', `${revision}^{commit}`], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 });
179
+ return output.status === 0 && typeof output.stdout === 'string' && output.stdout.trim() ? output.stdout.trim() : undefined;
180
+ }
181
+
174
182
  function gitHead(root: string): string | undefined {
175
- const output = spawnSync('git', ['-C', root, 'rev-parse', '--verify', 'HEAD'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 });
176
- return output.status === 0 && typeof output.stdout === 'string' ? output.stdout.trim() : undefined;
183
+ return gitRevision(root, 'HEAD');
177
184
  }
178
185
 
179
186
  function gitCommit(root: string, revision: string, label: string): string {
@@ -196,6 +203,250 @@ function gitChangedPaths(root: string, previousHead: string, candidateHead: stri
196
203
  return [...new Set(output.stdout.split('\0').filter(Boolean))].sort((left, right) => left.localeCompare(right));
197
204
  }
198
205
 
206
+ function gitMergeBase(root: string, leftHead: string, rightHead: string): string {
207
+ const output = spawnSync('git', ['-C', root, 'merge-base', leftHead, rightHead], {
208
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
209
+ });
210
+ if (output.status !== 0 || output.error || typeof output.stdout !== 'string' || !output.stdout.trim()) {
211
+ throw new Error('WORK_HEAD_ADOPTION_SCOPE_BASE_UNAVAILABLE');
212
+ }
213
+ return output.stdout.trim();
214
+ }
215
+
216
+ function gitIsAncestor(root: string, ancestor: string, descendant: string): boolean {
217
+ const result = spawnSync('git', ['-C', root, 'merge-base', '--is-ancestor', ancestor, descendant], {
218
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
219
+ });
220
+ if (result.error || (result.status !== 0 && result.status !== 1)) {
221
+ throw new Error(`WORK_TARGET_ADVANCE_ANCESTRY_UNAVAILABLE: ${ancestor} -> ${descendant}`);
222
+ }
223
+ return result.status === 0;
224
+ }
225
+
226
+ export interface WorkTargetAdvanceInspection {
227
+ relation: 'candidate_contains_target' | 'target_contains_candidate' | 'diverged_clean' | 'diverged_conflict';
228
+ candidateHead: string;
229
+ targetHead: string;
230
+ mergeBase: string;
231
+ candidateChangedPaths: string[];
232
+ targetChangedPaths: string[];
233
+ mergedTree?: string;
234
+ detail?: string;
235
+ }
236
+
237
+ export function inspectWorkTargetAdvance(root: string, candidateRevision: string, targetRevision: string): WorkTargetAdvanceInspection {
238
+ const candidateHead = gitCommit(root, candidateRevision, 'TARGET_ADVANCE_CANDIDATE');
239
+ const targetHead = gitCommit(root, targetRevision, 'TARGET_ADVANCE_TARGET');
240
+ if (gitIsAncestor(root, targetHead, candidateHead)) {
241
+ return {
242
+ relation: 'candidate_contains_target', candidateHead, targetHead, mergeBase: targetHead,
243
+ candidateChangedPaths: gitChangedPaths(root, targetHead, candidateHead), targetChangedPaths: [],
244
+ };
245
+ }
246
+ if (gitIsAncestor(root, candidateHead, targetHead)) {
247
+ return {
248
+ relation: 'target_contains_candidate', candidateHead, targetHead, mergeBase: candidateHead,
249
+ candidateChangedPaths: [], targetChangedPaths: gitChangedPaths(root, candidateHead, targetHead),
250
+ };
251
+ }
252
+ const mergeBase = gitMergeBase(root, candidateHead, targetHead);
253
+ const candidateChangedPaths = gitChangedPaths(root, mergeBase, candidateHead);
254
+ const targetChangedPaths = gitChangedPaths(root, mergeBase, targetHead);
255
+ const preflight = spawnSync('git', ['-C', root, 'merge-tree', '--write-tree', candidateHead, targetHead], {
256
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: 512 * 1024,
257
+ });
258
+ if (preflight.error || (preflight.status !== 0 && preflight.status !== 1)) {
259
+ throw new Error(`WORK_TARGET_ADVANCE_PREFLIGHT_UNAVAILABLE: ${preflight.error?.message ?? preflight.stderr ?? 'merge-tree failed'}`);
260
+ }
261
+ if (preflight.status === 0) {
262
+ const mergedTree = typeof preflight.stdout === 'string' ? preflight.stdout.trim().split(/\s+/)[0] : undefined;
263
+ return {
264
+ relation: 'diverged_clean', candidateHead, targetHead, mergeBase, candidateChangedPaths, targetChangedPaths,
265
+ ...(mergedTree ? { mergedTree } : {}),
266
+ };
267
+ }
268
+ const detail = `${preflight.stdout ?? ''}\n${preflight.stderr ?? ''}`.trim().slice(0, 1_000);
269
+ return { relation: 'diverged_conflict', candidateHead, targetHead, mergeBase, candidateChangedPaths, targetChangedPaths, ...(detail ? { detail } : {}) };
270
+ }
271
+
272
+ export function targetAdvanceLinearMergeCommits(root: string, targetRevision: string, candidateRevision: string): string[] {
273
+ const targetHead = gitCommit(root, targetRevision, 'TARGET_ADVANCE_LINEAR_TARGET');
274
+ const candidateHead = gitCommit(root, candidateRevision, 'TARGET_ADVANCE_LINEAR_CANDIDATE');
275
+ const output = spawnSync('git', ['-C', root, 'rev-list', '--merges', `${targetHead}..${candidateHead}`], {
276
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: 512 * 1024,
277
+ });
278
+ if (output.status !== 0 || output.error || typeof output.stdout !== 'string') {
279
+ throw new Error('WORK_TARGET_ADVANCE_LINEAR_HISTORY_UNAVAILABLE: could not inspect candidate merge history');
280
+ }
281
+ return output.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
282
+ }
283
+
284
+ function normalizedReadScope(scope: string): string {
285
+ return scope.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '') || '.';
286
+ }
287
+
288
+ function pathIsWithinReadScope(path: string, scope: string): boolean {
289
+ const normalized = normalizedReadScope(scope);
290
+ return normalized === '.' || path === normalized || path.startsWith(`${normalized}/`);
291
+ }
292
+
293
+ function stableCheckDefinition(check: ControllerCheck | undefined): string | undefined {
294
+ if (!check) return undefined;
295
+ return JSON.stringify({
296
+ id: check.id,
297
+ command: check.command,
298
+ cwd: check.cwd,
299
+ timeoutMs: check.timeoutMs,
300
+ source: check.source,
301
+ effects: check.effects ?? null,
302
+ });
303
+ }
304
+
305
+ export interface TargetAdvanceValidationTransferPlan {
306
+ transferredRecords: VerificationRecord[];
307
+ reusableCheckIds: string[];
308
+ invalidatedCheckIds: string[];
309
+ }
310
+
311
+ export function planTargetAdvanceValidationAuthority(input: {
312
+ checkIds: string[];
313
+ checkRefs: VerificationRecord[];
314
+ checksBefore: ControllerCheck[];
315
+ checksAfter: ControllerCheck[];
316
+ candidateHead: string;
317
+ candidateWorkspaceFingerprint: string;
318
+ integratedHead: string;
319
+ integratedWorkspaceFingerprint: string;
320
+ targetChangedPaths: string[];
321
+ recordedAt?: string;
322
+ }): TargetAdvanceValidationTransferPlan {
323
+ const beforeById = new Map(input.checksBefore.map((check) => [check.id, check]));
324
+ const afterById = new Map(input.checksAfter.map((check) => [check.id, check]));
325
+ const transferredRecords: VerificationRecord[] = [];
326
+ const reusableCheckIds: string[] = [];
327
+ const invalidatedCheckIds: string[] = [];
328
+ const recordedAt = input.recordedAt ?? new Date().toISOString();
329
+ for (const checkId of input.checkIds) {
330
+ const before = beforeById.get(checkId);
331
+ const after = afterById.get(checkId);
332
+ const reads = before?.effects?.reads;
333
+ const definitionUnchanged = stableCheckDefinition(before) !== undefined
334
+ && stableCheckDefinition(before) === stableCheckDefinition(after);
335
+ const readsGitHistory = before?.effects?.git !== undefined;
336
+ const targetTouchesReadScope = !reads || reads.length === 0
337
+ || input.targetChangedPaths.some((path) => reads.some((scope) => pathIsWithinReadScope(path, scope)));
338
+ const sourcePass = effectiveVerificationEvidence(input.checkRefs, {
339
+ sourceRevision: input.candidateHead,
340
+ workspaceFingerprint: input.candidateWorkspaceFingerprint,
341
+ checkId,
342
+ requestedChecks: input.checkIds,
343
+ }).find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))?.record;
344
+ if (!definitionUnchanged || readsGitHistory || targetTouchesReadScope || !sourcePass) {
345
+ invalidatedCheckIds.push(checkId);
346
+ continue;
347
+ }
348
+ const transferred: VerificationRecord = {
349
+ ...sourcePass,
350
+ summary: `Validation authority transferred across target advancement because target-only changes did not intersect check ${checkId} read inputs.`,
351
+ recordedAt,
352
+ sourceRevision: input.integratedHead,
353
+ workspaceFingerprint: input.integratedWorkspaceFingerprint,
354
+ verificationInputFingerprint: verificationInputFingerprint({
355
+ sourceRevision: input.integratedHead,
356
+ workspaceFingerprint: input.integratedWorkspaceFingerprint,
357
+ checkId,
358
+ requestedChecks: input.checkIds,
359
+ }),
360
+ evidenceRef: {
361
+ title: checkId,
362
+ summary: `Reused prior valid receipt after proving target-only path changes do not affect this check's declared inputs.`,
363
+ detailLevel: 'summary',
364
+ },
365
+ };
366
+ transferredRecords.push(transferred);
367
+ reusableCheckIds.push(checkId);
368
+ }
369
+ return { transferredRecords, reusableCheckIds, invalidatedCheckIds };
370
+ }
371
+
372
+ export function targetAdvanceWorkScopeViolation(
373
+ scope: { allowedPaths: string[]; forbiddenPaths: string[] } | undefined,
374
+ changedPaths: string[],
375
+ ): { kind: 'forbidden' | 'out_of_scope'; path: string } | undefined {
376
+ if (!scope) return undefined;
377
+ for (const path of changedPaths) {
378
+ if (scope.forbiddenPaths.some((pattern) => globMatches(pattern, path))) return { kind: 'forbidden', path };
379
+ if (scope.allowedPaths.length > 0 && !scope.allowedPaths.some((pattern) => globMatches(pattern, path))) {
380
+ return { kind: 'out_of_scope', path };
381
+ }
382
+ }
383
+ return undefined;
384
+ }
385
+
386
+ function replaceTargetAdvanceScopeEvidence(
387
+ ctx: MultiRepositoryMcpToolContext,
388
+ contract: NonNullable<ReturnType<typeof contractFor>>,
389
+ actualChangedPaths: string[],
390
+ ): void {
391
+ updateWorkContract({ controllerHome: ctx.controllerHome, repoId: contract.repoId }, contract.workId, {
392
+ scopeEvidence: {
393
+ initialLikelyPaths: contract.scopeEvidence?.initialLikelyPaths ?? contract.allowedPaths,
394
+ inspectedPaths: contract.scopeEvidence?.inspectedPaths ?? [],
395
+ actualChangedPaths: [...new Set(actualChangedPaths)].sort((left, right) => left.localeCompare(right)).slice(0, 500),
396
+ recordedAt: new Date().toISOString(),
397
+ },
398
+ });
399
+ }
400
+
401
+ export interface DirectTargetDeliveryInspection {
402
+ integrated: boolean;
403
+ reason: 'not_direct_target' | 'path_mismatch' | 'branch_mismatch' | 'dirty' | 'revision_unavailable' | 'not_reachable' | 'integrated';
404
+ expectedHead?: string;
405
+ targetHead?: string;
406
+ }
407
+
408
+ /**
409
+ * Direct-edit Work may already be executing on the delivery branch. In that
410
+ * case Git merge is not a delivery operation at all: the Work commit is
411
+ * integrated once its exact expected revision remains reachable from the
412
+ * target branch. Keep this proof deliberately strict so a feature Work or a
413
+ * dirty/mismatched checkout still follows the normal fail-closed merge path.
414
+ */
415
+ export function inspectDirectTargetDelivery(
416
+ root: string,
417
+ worktreePath: string,
418
+ managedWorktree: boolean,
419
+ workBranch: string,
420
+ targetBranch: string,
421
+ expectedRevision: string | undefined,
422
+ ): DirectTargetDeliveryInspection {
423
+ if (managedWorktree || workBranch !== targetBranch) return { integrated: false, reason: 'not_direct_target' };
424
+ try {
425
+ if (realpathSync(root) !== realpathSync(worktreePath)) return { integrated: false, reason: 'path_mismatch' };
426
+ } catch {
427
+ return { integrated: false, reason: 'path_mismatch' };
428
+ }
429
+ const branch = spawnSync('git', ['-C', root, 'branch', '--show-current'], {
430
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
431
+ });
432
+ if (branch.status !== 0 || typeof branch.stdout !== 'string' || branch.stdout.trim() !== targetBranch) {
433
+ return { integrated: false, reason: 'branch_mismatch' };
434
+ }
435
+ const status = spawnSync('git', ['-C', root, 'status', '--porcelain'], {
436
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
437
+ });
438
+ if (status.status !== 0 || typeof status.stdout !== 'string' || status.stdout.trim()) {
439
+ return { integrated: false, reason: 'dirty' };
440
+ }
441
+ const expectedHead = expectedRevision ? gitRevision(root, expectedRevision) : undefined;
442
+ const targetHead = gitRevision(root, targetBranch);
443
+ if (!expectedHead || !targetHead) return { integrated: false, reason: 'revision_unavailable' };
444
+ if (!gitIsAncestor(root, expectedHead, targetHead)) {
445
+ return { integrated: false, reason: 'not_reachable', expectedHead, targetHead };
446
+ }
447
+ return { integrated: true, reason: 'integrated', expectedHead, targetHead };
448
+ }
449
+
199
450
  function normalizedRequiredString(args: Record<string, unknown>, key: string): string | undefined {
200
451
  return typeof args[key] === 'string' && args[key].trim() ? args[key].trim() : undefined;
201
452
  }
@@ -230,6 +481,42 @@ function workReturnCheckoutId(
230
481
  return target.activeCheckoutId ?? repository.activeCheckoutId ?? fallbackCheckoutId;
231
482
  }
232
483
 
484
+ function exactCommitChangedPaths(repoRoot: string, revision: string): string[] {
485
+ const result = spawnSync('git', ['-C', repoRoot, 'diff-tree', '--root', '--no-commit-id', '--name-only', '-r', revision], {
486
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
487
+ });
488
+ if (result.status !== 0 || result.error || typeof result.stdout !== 'string') {
489
+ throw new Error(`WORK_COMPLETION_RECEIPT_COMMIT_PATHS_UNAVAILABLE: ${revision}`);
490
+ }
491
+ return Array.from(new Set(result.stdout.split('\n').map((entry) => entry.trim()).filter(Boolean)))
492
+ .sort((left, right) => left.localeCompare(right));
493
+ }
494
+
495
+ function workOwnedDirtyPaths(
496
+ contract: ReturnType<typeof contractFor>,
497
+ status: ReturnType<typeof repositoryGitStatus>,
498
+ ): { dirtyPaths: string[]; ownedPaths: string[] } {
499
+ const dirtyPaths = [...new Set([...status.staged, ...status.unstaged, ...status.untracked])]
500
+ .sort((left, right) => left.localeCompare(right));
501
+ if (!contract) return { dirtyPaths, ownedPaths: dirtyPaths };
502
+ const ownedPaths = dirtyPaths.filter((path) => {
503
+ if (contract.forbiddenPaths.some((pattern) => globMatches(pattern, path))) return false;
504
+ return contract.allowedPaths.length === 0 || contract.allowedPaths.some((pattern) => globMatches(pattern, path));
505
+ });
506
+ return { dirtyPaths, ownedPaths };
507
+ }
508
+
509
+ export function completionReceiptChangedPaths(repoRoot: string, baseRevision: string, deliveryRevision: string): string[] {
510
+ const result = spawnSync('git', ['-C', repoRoot, 'diff', '--name-only', `${baseRevision}..${deliveryRevision}`], {
511
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
512
+ });
513
+ if (result.status !== 0 || result.error || typeof result.stdout !== 'string') {
514
+ throw new Error(`WORK_COMPLETION_RECEIPT_CHANGED_PATHS_UNAVAILABLE: ${baseRevision}..${deliveryRevision}`);
515
+ }
516
+ return Array.from(new Set(result.stdout.split('\n').map((entry) => entry.trim()).filter(Boolean)))
517
+ .sort((left, right) => left.localeCompare(right));
518
+ }
519
+
233
520
  function completionReceiptForFinalizedWork(
234
521
  ctx: MultiRepositoryMcpToolContext,
235
522
  handle: WorkHandleState,
@@ -241,21 +528,31 @@ function completionReceiptForFinalizedWork(
241
528
  const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
242
529
  ? args.target_branch.trim()
243
530
  : repository.defaultBranch || 'main';
244
- const targetRevision = gitHead(target.canonicalRoot);
245
- if (!targetRevision) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED: target HEAD is unavailable');
246
- const reachable = spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', targetRevision, targetBranch], {
531
+ const targetRevision = gitRevision(target.canonicalRoot, targetBranch);
532
+ if (!targetRevision) throw new Error(`WORK_COMPLETION_RECEIPT_TARGET_REQUIRED: target branch ${targetBranch} is unavailable`);
533
+ const noChange = args.completion_outcome === 'completed_no_change';
534
+ // Delivery authority is always the Work's own revision, never the target
535
+ // branch's newest HEAD. The target may have advanced because of unrelated
536
+ // concurrent Work; targetRevision records integration identity, while
537
+ // deliveryRevision owns path attribution and reachability proof.
538
+ const deliveryRevision = handle.expectedHead ?? handle.baseCommit;
539
+ if (!deliveryRevision) throw new Error('WORK_COMPLETION_RECEIPT_SOURCE_REQUIRED: Work delivery revision is unavailable');
540
+ const reachable = deliveryRevision === targetRevision || spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', deliveryRevision, targetBranch], {
247
541
  encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
248
542
  }).status === 0;
249
- if (!reachable) throw new Error(`WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN: ${targetRevision} is not reachable from ${targetBranch}`);
250
- const changedPaths = handle.baseCommit
251
- ? Array.from(new Set(String(spawnSync('git', ['-C', target.canonicalRoot, 'diff', '--name-only', `${handle.baseCommit}..${targetRevision}`], {
252
- encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
253
- }).stdout ?? '').split('\n').map((entry) => entry.trim()).filter(Boolean))).sort((left, right) => left.localeCompare(right))
254
- : [];
543
+ if (!reachable) throw new Error(`WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN: ${deliveryRevision} is not reachable from ${targetBranch}`);
544
+ const deliveryBaseRevision = workDeliveryBaseRevision(handle);
545
+ const changedPaths = noChange
546
+ ? []
547
+ : !handle.managedWorktree && handle.finalization.commit === 'done'
548
+ ? exactCommitChangedPaths(target.canonicalRoot, deliveryRevision)
549
+ : deliveryBaseRevision
550
+ ? completionReceiptChangedPaths(target.canonicalRoot, deliveryBaseRevision, deliveryRevision)
551
+ : (() => { throw new Error('WORK_COMPLETION_RECEIPT_BASE_REQUIRED: Work delivery provenance base is unavailable'); })();
255
552
  const recordedAt = new Date().toISOString();
256
- const noChange = args.completion_outcome === 'completed_no_change';
553
+ const retainsExplicitDeliveryBranch = args.merge !== true && targetBranch === handle.branch;
257
554
  const warnings = [
258
- ...(handle.finalization.branchCleanup === 'skipped' ? [{ code: 'cleanup_retained_by_request' as const, message: 'Branch cleanup was skipped by the finalization request.', resourceKind: 'branch' as const, resourceId: handle.branch, recordedAt }] : []),
555
+ ...(handle.finalization.branchCleanup === 'skipped' && !retainsExplicitDeliveryBranch ? [{ code: 'cleanup_retained_by_request' as const, message: 'Branch cleanup was skipped by the finalization request.', resourceKind: 'branch' as const, resourceId: handle.branch, recordedAt }] : []),
259
556
  ...(handle.finalization.worktreeCleanup === 'skipped' && handle.managedWorktree ? [{ code: 'cleanup_retained_by_request' as const, message: 'Managed worktree cleanup was skipped by the finalization request.', resourceKind: 'worktree' as const, resourceId: handle.worktreePath, recordedAt }] : []),
260
557
  ];
261
558
  return {
@@ -267,7 +564,7 @@ function completionReceiptForFinalizedWork(
267
564
  workId: handle.workId,
268
565
  targetBranch,
269
566
  targetRevision,
270
- sourceRevision: handle.baseCommit,
567
+ sourceRevision: deliveryRevision,
271
568
  baseRevision: handle.baseCommit,
272
569
  changedPaths,
273
570
  delivery: {
@@ -302,6 +599,7 @@ function workPrepareFingerprint(input: {
302
599
  allowedPaths: string[];
303
600
  checks: string[];
304
601
  baseRef?: string;
602
+ needsDependencies: boolean;
305
603
  }): string {
306
604
  return createHash('sha256').update(JSON.stringify({ schemaVersion: 1, operation: 'work_prepare', ...input })).digest('hex');
307
605
  }
@@ -363,14 +661,30 @@ function findWorkHandle(
363
661
  });
364
662
  }
365
663
 
664
+ function currentControllerClaimAuthorizesTerminalCleanup(
665
+ ctx: MultiRepositoryMcpToolContext,
666
+ session: ExecutionSessionContext,
667
+ handle: WorkHandleState,
668
+ ): boolean {
669
+ if (!terminalCleanupOutcome(ctx, handle)) return false;
670
+ const workIdValue = handle.workContractId ?? handle.workId;
671
+ const owner = getControllerSession({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, workIdValue);
672
+ if (!owner) return false;
673
+ return (owner.principalId?.trim() || owner.controllerId) === session.principalId
674
+ && owner.controllerInstanceId === session.controllerInstanceId;
675
+ }
676
+
366
677
  function workForSession(
367
678
  ctx: MultiRepositoryMcpToolContext,
368
679
  session: ExecutionSessionContext,
369
680
  args: Record<string, unknown>,
370
- options: { reconcileValidation?: boolean } = {},
681
+ options: { reconcileValidation?: boolean; allowClaimedTerminalCleanup?: boolean } = {},
371
682
  ): WorkHandleState {
372
683
  let handle = findWorkHandle(ctx, session, args);
373
- if (handle.principalId !== session.principalId) throw new Error('WORK_HANDLE_PRINCIPAL_MISMATCH: work handle belongs to another principal');
684
+ if (
685
+ handle.principalId !== session.principalId
686
+ && !(options.allowClaimedTerminalCleanup === true && currentControllerClaimAuthorizesTerminalCleanup(ctx, session, handle))
687
+ ) throw new Error('WORK_HANDLE_PRINCIPAL_MISMATCH: work handle belongs to another principal');
374
688
  if (options.reconcileValidation !== false) handle = reconcileWorkValidation(ctx.controllerHome, handle).handle;
375
689
  if (
376
690
  session.activeRepositoryId !== handle.repositoryId
@@ -393,27 +707,53 @@ function assertWorkControllerOwnership(
393
707
  session: ExecutionSessionContext,
394
708
  handle: WorkHandleState,
395
709
  args: Record<string, unknown>,
396
- ): void {
710
+ ) {
397
711
  const workIdValue = handle.workContractId ?? handle.workId;
398
- const owner = getControllerSession({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, workIdValue);
712
+ const options = { controllerHome: ctx.controllerHome, repoId: handle.repositoryId };
713
+ const owner = getControllerSession(options, workIdValue);
399
714
  const controllerId = typeof args.controller_id === 'string' && args.controller_id.trim()
400
715
  ? args.controller_id.trim()
401
716
  : session.principalId;
402
717
  if (controllerId !== session.principalId) {
403
718
  throw new Error('WORK_CONTROLLER_IDENTITY_MISMATCH: controller_id must match the authenticated principal');
404
719
  }
405
- const resumed = resumeControllerSession({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, {
720
+ if (owner) {
721
+ const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
722
+ const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
723
+ if (owner.controllerId !== controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workIdValue} is owned by ${owner.controllerId}`);
724
+ if (ownerPrincipal !== session.principalId) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workIdValue}`);
725
+ if (!ownerInstanceId || ownerInstanceId !== session.controllerInstanceId) throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workIdValue}`);
726
+ if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workIdValue}`);
727
+ const resumed = resumeControllerSession(options, {
728
+ workId: workIdValue,
729
+ controllerId,
730
+ controllerType: owner.controllerType,
731
+ sessionId: session.sessionId,
732
+ principalId: session.principalId,
733
+ controllerInstanceId: session.controllerInstanceId,
734
+ expectedClaimGeneration: owner.claimGeneration,
735
+ leaseMs: 3_600_000,
736
+ });
737
+ if (
738
+ resumed.controllerId !== controllerId
739
+ || resumed.principalId !== session.principalId
740
+ || resumed.controllerInstanceId !== session.controllerInstanceId
741
+ || resumed.claimGeneration !== owner.claimGeneration
742
+ ) {
743
+ throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workIdValue} ownership epoch changed during resume`);
744
+ }
745
+ return resumed;
746
+ }
747
+ return claimControllerSession(options, {
406
748
  workId: workIdValue,
407
749
  controllerId,
408
- controllerType: owner?.controllerType ?? 'chatgpt',
750
+ controllerType: 'chatgpt',
409
751
  sessionId: session.sessionId,
410
752
  principalId: session.principalId,
411
753
  controllerInstanceId: session.controllerInstanceId,
754
+ expectedClaimGeneration: 0,
412
755
  leaseMs: 3_600_000,
413
756
  });
414
- if (resumed.controllerId !== controllerId || resumed.sessionId !== session.sessionId) {
415
- throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workIdValue} is owned by ${resumed.controllerId}/${resumed.sessionId}`);
416
- }
417
757
  }
418
758
 
419
759
  function claimPreparedWorkOwnership(
@@ -552,7 +892,16 @@ function adoptExistingWorkHead(
552
892
  if (isTerminalWorkContractStatus(contract.status) || contract.completionReceipt) {
553
893
  throw new Error('WORK_HEAD_ADOPTION_CONTRACT_TERMINAL');
554
894
  }
555
- const changedPaths = gitChangedPaths(handle.worktreePath, previousHead, candidateHead);
895
+ // expectedHead can predate unrelated commits that landed on the source checkout
896
+ // before this Work was rebased. Scope adoption to the candidate's unique delta
897
+ // from the current source-checkout merge-base; otherwise those target-branch
898
+ // commits are incorrectly attributed to the Work and fail its allow-list.
899
+ const sourceRepository = selectWorkFinalizationTarget(registered, handle);
900
+ const sourceHead = handle.sourceCheckoutId && handle.sourceCheckoutId !== handle.checkoutId
901
+ ? repositoryGitStatus(sourceRepository).head
902
+ : undefined;
903
+ const scopeBaseHead = sourceHead ? gitMergeBase(handle.worktreePath, sourceHead, candidateHead) : previousHead;
904
+ const changedPaths = gitChangedPaths(handle.worktreePath, scopeBaseHead, candidateHead);
556
905
  for (const path of changedPaths) {
557
906
  if (contract.forbiddenPaths.some((pattern) => globMatches(pattern, path))) {
558
907
  throw new Error(`WORK_HEAD_ADOPTION_FORBIDDEN_PATH: ${path}`);
@@ -587,6 +936,7 @@ function adoptExistingWorkHead(
587
936
  };
588
937
 
589
938
  const adopted = transitionWorkHandle(ctx.controllerHome, handle, 'editing', {
939
+ deliveryBaseCommit: scopeBaseHead,
590
940
  expectedHead: candidateHead,
591
941
  failureReason: undefined,
592
942
  finalization: initialStage(),
@@ -604,7 +954,7 @@ function adoptExistingWorkHead(
604
954
  });
605
955
  appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, contract.workId, {
606
956
  title: 'audited WorkHandle successor HEAD adoption',
607
- summary: `${previousHead} -> ${candidateHead}; ${changedPaths.length} changed path(s) remained within the WorkContract allow-list. Historical validation and completion receipts were not rewritten.`,
957
+ summary: `${previousHead} -> ${candidateHead}; ${changedPaths.length} candidate-unique path(s) from scope base ${scopeBaseHead} remained within the WorkContract allow-list. Historical validation and completion receipts were not rewritten.`,
608
958
  detailLevel: 'summary',
609
959
  });
610
960
  } catch (error) {
@@ -632,21 +982,43 @@ function adoptExistingWorkHead(
632
982
  };
633
983
  }
634
984
 
635
- function releasePreparedWorkOwnership(
985
+ export function releasePreparedWorkOwnership(
636
986
  ctx: MultiRepositoryMcpToolContext,
637
987
  handle: WorkHandleState,
638
988
  ): 'released' | 'already_released' {
639
989
  const workIdValue = handle.workContractId ?? handle.workId;
640
- const current = getControllerSession(
641
- { controllerHome: ctx.controllerHome, repoId: handle.repositoryId },
642
- workIdValue,
643
- );
990
+ const options = { controllerHome: ctx.controllerHome, repoId: handle.repositoryId };
991
+ const current = getControllerSession(options, workIdValue);
644
992
  if (!current) return 'already_released';
645
- releaseControllerSession(
646
- { controllerHome: ctx.controllerHome, repoId: handle.repositoryId },
647
- workIdValue,
648
- current.controllerId,
649
- );
993
+
994
+ const callerPrincipal = principalFor(ctx);
995
+ const callerInstanceId = ctx.controllerInstanceId ?? currentControllerInstanceId();
996
+ const ownerPrincipal = current.principalId?.trim() || current.controllerId;
997
+ const ownerInstanceId = current.controllerInstanceId?.trim() || '';
998
+ if (ownerPrincipal !== callerPrincipal) {
999
+ throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workIdValue}`);
1000
+ }
1001
+ if (!ownerInstanceId || ownerInstanceId !== callerInstanceId) {
1002
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workIdValue}`);
1003
+ }
1004
+ if (typeof current.claimGeneration !== 'number' || current.claimGeneration < 1) {
1005
+ throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workIdValue}`);
1006
+ }
1007
+
1008
+ const released = releaseControllerSessionWithAuthority(options, {
1009
+ workId: workIdValue,
1010
+ actor: `legacy-work-release:${callerPrincipal}:${callerInstanceId}`,
1011
+ authority: {
1012
+ controllerId: current.controllerId,
1013
+ controllerType: current.controllerType,
1014
+ principalId: ownerPrincipal,
1015
+ controllerInstanceId: ownerInstanceId,
1016
+ claimGeneration: current.claimGeneration,
1017
+ },
1018
+ });
1019
+ if (!released.allowed) {
1020
+ throw new Error(`WORK_CONTROLLER_RELEASE_FENCED: ${workIdValue}:${released.reason}`);
1021
+ }
650
1022
  return 'released';
651
1023
  }
652
1024
 
@@ -657,9 +1029,14 @@ function terminalCleanupOutcome(
657
1029
  if (handle.cleanupReceipt) return handle.cleanupReceipt.terminalOutcome;
658
1030
  const contract = contractFor(ctx, handle);
659
1031
  if (contract?.status === 'cancelled') return 'cancelled';
1032
+ if (contract?.status === 'completed') return 'completed_cleanup';
660
1033
  const reason = `${handle.failureReason ?? ''} ${handle.finalization.lastError ?? ''}`.toLowerCase();
661
1034
  if (contract?.status === 'blocked' && reason.includes('terminal')) return 'blocked_terminal';
662
- if (contract?.status === 'failed' || handle.state === 'failed' || handle.state === 'failed_terminal_cleanup') {
1035
+ // `failed` is a retryable execution-handle state (its transition table allows
1036
+ // failed -> validating/editing). Do not reinterpret it as terminal cleanup
1037
+ // unless the durable Work itself is terminal. `failed_terminal_cleanup` is
1038
+ // the explicit point of no return for resource cleanup reconciliation.
1039
+ if (contract?.status === 'failed' || handle.state === 'failed_terminal_cleanup') {
663
1040
  if (reason.includes('infrastructure') || reason.includes('timed out') || reason.includes('unavailable')) {
664
1041
  return 'infrastructure_failed';
665
1042
  }
@@ -772,6 +1149,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
772
1149
  const checks = boundedStringArray(args.checks, 30);
773
1150
  const baseRef = typeof args.base_ref === 'string' && args.base_ref.trim() ? args.base_ref.trim() : undefined;
774
1151
  const requestedCheckoutId = typeof args.checkout_id === 'string' && args.checkout_id.trim() ? args.checkout_id.trim() : undefined;
1152
+ const needsDependencies = args.needs_dependencies === true;
775
1153
  const baseCheckoutId = repository.activeCheckoutId;
776
1154
  const baseStatus = repositoryGitStatus(repository);
777
1155
  if (isolation === 'reuse' && !baseStatus.clean) throw new Error('WORKTREE_DIRTY: reuse was requested but the selected checkout is dirty; choose new_worktree or auto');
@@ -787,6 +1165,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
787
1165
  allowedPaths,
788
1166
  checks,
789
1167
  baseRef,
1168
+ needsDependencies,
790
1169
  });
791
1170
 
792
1171
  return withWorkPrepareRequest({
@@ -872,7 +1251,12 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
872
1251
  });
873
1252
  try {
874
1253
  const workspace = useWorktree
875
- ? ensureManagedWorkspace(ctx.controllerHome, repository, { requestId: createdWorkId, title: objective, baseRef })
1254
+ ? ensureManagedWorkspace(ctx.controllerHome, repository, {
1255
+ requestId: createdWorkId,
1256
+ title: objective,
1257
+ baseRef,
1258
+ prepareDependencies: needsDependencies,
1259
+ })
876
1260
  : { mode: 'current' as const, checkoutId: baseCheckoutId, root: repository.canonicalRoot, branch: baseStatus.branch ?? 'detached', baseRevision: baseStatus.head ?? undefined, managed: false };
877
1261
  const refreshed = getRepository(repository.repoId, ctx.controllerHome);
878
1262
  const checkout = selectRepositoryCheckout(refreshed, workspace.checkoutId);
@@ -883,7 +1267,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
883
1267
  schemaVersion: 1, workId: createdWorkId, sessionId: session.sessionId, principalId: session.principalId,
884
1268
  repositoryId: repository.repoId, checkoutId: checkout.activeCheckoutId, worktreePath: checkout.canonicalRoot, branch,
885
1269
  sourceCheckoutId: baseCheckoutId, managedWorktree: workspace.managed, workContractId: contract.workId, goalId, delegationVersion: delegation.version,
886
- baseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
1270
+ baseCommit: workspace.baseRevision ?? head, deliveryBaseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
887
1271
  state: 'prepared', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), finalization: initialStage(),
888
1272
  cleanupResponsibility: { owner: 'work_finalizer', registeredAt: new Date().toISOString() },
889
1273
  };
@@ -1040,15 +1424,16 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
1040
1424
  maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : undefined,
1041
1425
  });
1042
1426
  const process = execution.process;
1043
- const completed = process?.completed === true || execution.route === 'process_direct';
1044
1427
  const ok = process ? process.ok === true : execution.ok === true;
1045
1428
  const status = process
1046
1429
  ? (process.completed
1047
1430
  ? (process.cancelled ? 'cancelled' : process.timedOut ? 'timed_out' : ok ? 'executed' : 'failed')
1048
1431
  : 'running')
1049
- : execution.route === 'durable'
1050
- ? 'deferred_durable'
1051
- : 'rejected';
1432
+ : execution.route === 'process_direct'
1433
+ ? (ok ? 'executed' : 'failed')
1434
+ : execution.route === 'durable'
1435
+ ? 'deferred_durable'
1436
+ : 'rejected';
1052
1437
  return {
1053
1438
  processId: process?.processId,
1054
1439
  commandId,
@@ -1082,8 +1467,9 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
1082
1467
  for (const [index, entry] of inputs.entries()) {
1083
1468
  const execution = await run(entry, index);
1084
1469
  ordered.push(execution);
1085
- // Do not launch another mutating command while this Work-owned Process
1086
- // still owns workspace leases. The caller resumes through process_wait.
1470
+ // Do not launch another mutating command while a Work-owned Lightweight
1471
+ // process is still changing the workspace. This is sequencing, not a
1472
+ // durable Lease boundary; the caller resumes through process_wait.
1087
1473
  if (execution.process && !execution.process.completed) break;
1088
1474
  }
1089
1475
  return ordered;
@@ -1130,7 +1516,10 @@ export function selectDefaultWorkValidationChecks(
1130
1516
 
1131
1517
  async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
1132
1518
  const session = requireSession(ctx, args);
1133
- const handle = workForSession(ctx, session, args, { reconcileValidation: false });
1519
+ const handle = workForSession(ctx, session, args, {
1520
+ reconcileValidation: false,
1521
+ allowClaimedTerminalCleanup: args.cleanup !== false,
1522
+ });
1134
1523
  const terminalOutcome = terminalCleanupOutcome(ctx, handle);
1135
1524
  if (terminalOutcome && args.cleanup !== false) {
1136
1525
  return await reconcileTerminalCleanup(ctx, session, handle, args, terminalOutcome);
@@ -1182,6 +1571,24 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1182
1571
  checks.push({ checkId, ok: false, status: 'missing', summary: `Check not found: ${checkId}` });
1183
1572
  break;
1184
1573
  }
1574
+ const reusablePass = contract
1575
+ ? [...effectiveVerificationEvidence(contract.checkRefs, {
1576
+ sourceRevision: validationHead,
1577
+ workspaceFingerprint,
1578
+ checkId,
1579
+ requestedChecks,
1580
+ })].reverse().find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))
1581
+ : undefined;
1582
+ if (reusablePass) {
1583
+ checks.push({
1584
+ checkId,
1585
+ ok: true,
1586
+ status: 'passed',
1587
+ reusedEvidence: true,
1588
+ receipt: reusablePass.record.receipt,
1589
+ });
1590
+ continue;
1591
+ }
1185
1592
  const existingBinding = validationRun.processes[checkId];
1186
1593
  let process = existingBinding
1187
1594
  ? getCheckProcessHandle(ctx.controllerHome, handle.repositoryId, existingBinding.processId)
@@ -1202,23 +1609,36 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1202
1609
  repoId: handle.repositoryId,
1203
1610
  checkoutId: handle.checkoutId,
1204
1611
  repoRoot: validated.worktreeRepository.canonicalRoot,
1205
- executionIdentity: executionIdentityForWork(validated.worktreeRepository, handle),
1612
+ executionIdentity: Object.freeze({
1613
+ ...executionIdentityForWork(validated.worktreeRepository, handle),
1614
+ expectedHead: validationHead === 'unknown' ? handle.expectedHead : validationHead,
1615
+ }),
1206
1616
  checkId,
1207
1617
  requestId: processRequestId,
1208
1618
  workId: handle.workId,
1209
1619
  commandId: processRequestId,
1210
1620
  verificationBinding: { executionSessionId: session.sessionId },
1621
+ verificationSnapshot: contract && contract.allowedPaths.length > 0 ? {
1622
+ workId: contract.workId,
1623
+ allowedPaths: contract.allowedPaths,
1624
+ forbiddenPaths: contract.forbiddenPaths,
1625
+ } : undefined,
1211
1626
  });
1212
1627
  if (executed.mode === 'durable') {
1213
1628
  checks.push({ checkId, ok: undefined, status: 'deferred', summary: executed.durable?.reason, durable: executed.durable });
1214
1629
  break;
1215
1630
  }
1216
1631
  process = executed.process!;
1632
+ const launchedRecord = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
1217
1633
  validationRun = {
1218
1634
  ...validationRun,
1219
1635
  processes: {
1220
1636
  ...validationRun.processes,
1221
- [checkId]: { processId: process.processId, requestId: processRequestId },
1637
+ [checkId]: {
1638
+ processId: process.processId,
1639
+ requestId: processRequestId,
1640
+ ...(launchedRecord?.checkExecution ? { checkExecution: { ...launchedRecord.checkExecution } } : {}),
1641
+ },
1222
1642
  },
1223
1643
  };
1224
1644
  current = transitionWorkHandle(ctx.controllerHome, current, 'validating', { validationRun });
@@ -1232,17 +1652,32 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1232
1652
  checks.push({ checkId, ok: false, status: 'infrastructure_failure', summary: `Validation process record is unavailable: ${process.processId}` });
1233
1653
  break;
1234
1654
  }
1655
+ const boundCheckExecution = validationRun.processes[checkId]?.checkExecution;
1235
1656
  const receipt = processCheckCompletionReceipt(record, {
1236
1657
  repoId: handle.repositoryId,
1237
1658
  checkoutId: handle.checkoutId,
1238
1659
  workId: handle.workId,
1239
- executionSessionId: session.sessionId,
1240
1660
  checkId,
1241
1661
  processId: process.processId,
1662
+ ...(boundCheckExecution ? {
1663
+ checkExecution: {
1664
+ cacheKey: boundCheckExecution.cacheKey,
1665
+ revision: boundCheckExecution.revision,
1666
+ definitionDigest: boundCheckExecution.definitionDigest,
1667
+ environmentFingerprint: boundCheckExecution.environmentFingerprint,
1668
+ timeoutMs: boundCheckExecution.timeoutMs,
1669
+ scopeKey: boundCheckExecution.scopeKey,
1670
+ },
1671
+ } : {}),
1242
1672
  });
1673
+ const terminalEvidence = classifyPersistedCheckTerminalEvidence(record, checkId);
1674
+ const infrastructureFailed = receipt.timedOut
1675
+ || receipt.cancelled
1676
+ || terminalEvidence.state !== 'matched'
1677
+ || (!receipt.ok && terminalEvidence.failureClass !== 'acceptance_failure');
1243
1678
  appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, handle.workId, {
1244
1679
  checkId,
1245
- outcome: receipt.ok ? 'valid_pass' : receipt.status === 'timed_out' || receipt.status === 'cancelled' ? 'infrastructure_failure' : 'valid_fail',
1680
+ outcome: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'valid_pass' : 'valid_fail',
1246
1681
  summary: receipt.summary,
1247
1682
  recordedAt: receipt.finishedAt,
1248
1683
  sourceRevision: validationHead,
@@ -1252,7 +1687,6 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1252
1687
  workspaceFingerprint,
1253
1688
  checkId,
1254
1689
  requestedChecks,
1255
- commandId: receipt.commandId,
1256
1690
  }),
1257
1691
  commandFingerprint: commandFingerprint(checkId, receipt.commandId),
1258
1692
  resultArtifactId: receipt.receiptId,
@@ -1264,9 +1698,10 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1264
1698
  checks.push({
1265
1699
  checkId,
1266
1700
  ok: receipt.ok,
1267
- status: receipt.ok ? 'passed' : receipt.status === 'timed_out' || receipt.status === 'cancelled' ? 'infrastructure_failure' : 'failed',
1701
+ status: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'passed' : 'failed',
1268
1702
  process,
1269
1703
  receipt,
1704
+ ...(terminalEvidence.warning ? { warning: terminalEvidence.warning } : {}),
1270
1705
  });
1271
1706
  if (!receipt.ok) break;
1272
1707
  }
@@ -1311,21 +1746,25 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1311
1746
  failureSummary,
1312
1747
  );
1313
1748
  }
1314
- if (completed && !passed) {
1315
- const cleanup = await reconcileTerminalCleanup(
1316
- ctx,
1317
- session,
1318
- next,
1319
- args,
1320
- infrastructureFailure ? 'infrastructure_failed' : 'validation_failed',
1321
- );
1749
+ if (completed && acceptedFailure) {
1750
+ const cleanup = await reconcileTerminalCleanup(ctx, session, next, args, 'validation_failed');
1322
1751
  const value = {
1323
1752
  ...cleanup,
1324
1753
  validation: { passed, completed, checks, targeted: true, changedPaths, cleanupTriggered: true },
1325
1754
  };
1326
1755
  return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
1327
1756
  }
1328
- const value = { work: compactHandle(next), validation: { passed, completed, checks, targeted: true, changedPaths } };
1757
+ const value = {
1758
+ work: compactHandle(next),
1759
+ validation: {
1760
+ passed,
1761
+ completed,
1762
+ checks,
1763
+ targeted: true,
1764
+ changedPaths,
1765
+ ...(infrastructureFailure ? { retryable: true, cleanupTriggered: false } : {}),
1766
+ },
1767
+ };
1329
1768
  return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
1330
1769
  }
1331
1770
 
@@ -1390,7 +1829,6 @@ function cleanupOnlyMergedHead(
1390
1829
  encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
1391
1830
  });
1392
1831
  if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
1393
- if (current.expectedHead && currentHead === current.expectedHead && !cancelledContract) return undefined;
1394
1832
  if (current.expectedHead && currentHead !== current.expectedHead && cancelledContract) return undefined;
1395
1833
  return { currentHead, cancelledContract, worktreeMissing: false };
1396
1834
  }
@@ -1451,7 +1889,35 @@ function failedCleanupOnlyHead(
1451
1889
  return { currentHead: expectedHead, worktreeMissing: false, targetBranch };
1452
1890
  }
1453
1891
 
1454
- function resetFailedFinalizationStages(stages: WorkFinalizationStages, wants: { commit: boolean; merge: boolean; cleanup: boolean }): WorkFinalizationStages {
1892
+ type RetryableFinalizationStage = 'commit' | 'merge' | 'branchCleanup' | 'worktreeCleanup';
1893
+
1894
+ function requestedFailedFinalizationRetry(
1895
+ stages: WorkFinalizationStages,
1896
+ wants: { commit: boolean; merge: boolean; cleanup: boolean },
1897
+ ): RetryableFinalizationStage | undefined {
1898
+ if (stages.validation === 'failed') return undefined;
1899
+ if (wants.commit && stages.commit === 'failed') return 'commit';
1900
+ if (wants.merge && stages.merge === 'failed') return 'merge';
1901
+ if (wants.cleanup && stages.worktreeCleanup === 'failed') return 'worktreeCleanup';
1902
+ if (wants.cleanup && stages.branchCleanup === 'failed') return 'branchCleanup';
1903
+ return undefined;
1904
+ }
1905
+
1906
+ function retryPhaseForFinalizationStage(stage: RetryableFinalizationStage): 'delivery' | 'cleanup' {
1907
+ return stage === 'commit' || stage === 'merge' ? 'delivery' : 'cleanup';
1908
+ }
1909
+
1910
+ function retryHandleStateForFinalization(stages: WorkFinalizationStages): WorkHandleState['state'] {
1911
+ if (stages.merge === 'done') return 'merged';
1912
+ if (stages.commit === 'done') return 'committed';
1913
+ return 'validating';
1914
+ }
1915
+
1916
+ export function resetFinalizationStagesForRequest(
1917
+ stages: WorkFinalizationStages,
1918
+ wants: { commit: boolean; merge: boolean; cleanup: boolean },
1919
+ options: { managedWorktree?: boolean; deleteBranchRequested?: boolean; retainedByRequest?: boolean; workspaceDirty?: boolean } = {},
1920
+ ): WorkFinalizationStages {
1455
1921
  const next = { ...stages };
1456
1922
  let reset = false;
1457
1923
  if (next.validation === 'failed') {
@@ -1474,6 +1940,28 @@ function resetFailedFinalizationStages(stages: WorkFinalizationStages, wants: {
1474
1940
  next.branchCleanup = 'pending';
1475
1941
  reset = true;
1476
1942
  }
1943
+ // A Work may be semantically repaired and revalidated after an earlier delivery
1944
+ // attempt already marked commit/merge done. Exact validation of a dirty current
1945
+ // workspace proves there is new Work-owned content still requiring delivery;
1946
+ // re-arm only the requested Git stages instead of letting cleanup discard it.
1947
+ if (options.workspaceDirty === true && wants.commit && next.commit === 'done') {
1948
+ next.commit = 'pending';
1949
+ reset = true;
1950
+ if (wants.merge && next.merge === 'done') next.merge = 'pending';
1951
+ }
1952
+ // A prior finalize(cleanup=false) intentionally records managed resources as
1953
+ // skipped/retained. A later explicit cleanup=true is a new resource-disposal
1954
+ // request, not an idempotent replay of the earlier retention decision. Re-arm
1955
+ // only cleanup stages that are applicable to this managed Work; commit/merge
1956
+ // skipped states keep their original delivery semantics.
1957
+ if (wants.cleanup && options.retainedByRequest === true && options.managedWorktree === true && next.worktreeCleanup === 'skipped') {
1958
+ next.worktreeCleanup = 'pending';
1959
+ reset = true;
1960
+ }
1961
+ if (wants.cleanup && options.retainedByRequest === true && options.managedWorktree === true && options.deleteBranchRequested === true && next.branchCleanup === 'skipped') {
1962
+ next.branchCleanup = 'pending';
1963
+ reset = true;
1964
+ }
1477
1965
  if (reset) delete next.lastError;
1478
1966
  return next;
1479
1967
  }
@@ -1516,12 +2004,87 @@ function currentWorkValidationInput(
1516
2004
 
1517
2005
  async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
1518
2006
  const session = requireSession(ctx, args);
1519
- let current = workForSession(ctx, session, args);
2007
+ let current = workForSession(ctx, session, args, { allowClaimedTerminalCleanup: args.cleanup !== false });
2008
+ const requestedWants = { commit: args.commit === true, merge: args.merge === true, cleanup: args.cleanup === true };
2009
+ const retryStage = current.state === 'failed'
2010
+ ? requestedFailedFinalizationRetry(current.finalization, requestedWants)
2011
+ : undefined;
2012
+ const retryContract = retryStage ? contractFor(ctx, current) : undefined;
2013
+ if (
2014
+ retryStage
2015
+ && retryContract?.status === 'failed'
2016
+ && !retryContract.completionOutcome
2017
+ && !retryContract.completionReceipt
2018
+ ) {
2019
+ transitionWorkContractPhase(
2020
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
2021
+ retryContract.workId,
2022
+ {
2023
+ phase: retryPhaseForFinalizationStage(retryStage),
2024
+ status: 'running',
2025
+ state: 'active',
2026
+ summary: `Retrying previously failed Work finalization stage ${retryStage}.`,
2027
+ },
2028
+ );
2029
+ }
1520
2030
  const terminalOutcome = terminalCleanupOutcome(ctx, current);
1521
2031
  if (terminalOutcome && args.cleanup !== false) {
1522
2032
  return await reconcileTerminalCleanup(ctx, session, current, args, terminalOutcome);
1523
2033
  }
1524
- assertWorkControllerOwnership(ctx, session, current, args);
2034
+ const terminalizationOwner = assertWorkControllerOwnership(ctx, session, current, args);
2035
+ if (terminalOutcome && args.cleanup === false) {
2036
+ const recordedAt = new Date().toISOString();
2037
+ current = withControllerLock(
2038
+ ctx.controllerHome,
2039
+ { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId },
2040
+ `work-finalize:${current.workId}:retain-terminal-resources`,
2041
+ () => {
2042
+ const fresh = readWorkHandle(ctx.controllerHome, current.repositoryId, current.workId) ?? current;
2043
+ return writeWorkHandle(ctx.controllerHome, {
2044
+ ...fresh,
2045
+ finalization: {
2046
+ ...fresh.finalization,
2047
+ validation: fresh.finalization.validation === 'failed' ? 'failed' : 'done',
2048
+ commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
2049
+ merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
2050
+ branchCleanup: 'skipped',
2051
+ worktreeCleanup: 'skipped',
2052
+ lastError: fresh.finalization.validation === 'failed' ? fresh.finalization.lastError : undefined,
2053
+ },
2054
+ terminalResourceDisposition: {
2055
+ mode: 'retained_by_request',
2056
+ retainWorktree: fresh.managedWorktree,
2057
+ retainBranch: true,
2058
+ recordedAt,
2059
+ },
2060
+ });
2061
+ },
2062
+ 10_000,
2063
+ );
2064
+ appendWorkEvidence(
2065
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
2066
+ current.workContractId ?? current.workId,
2067
+ {
2068
+ title: 'terminal resources retained by request',
2069
+ summary: `Controller explicitly retained ${current.managedWorktree ? 'the managed worktree and ' : ''}local branch after terminal Work; automatic terminal cleanup must not reclaim them.`,
2070
+ detailLevel: 'summary',
2071
+ },
2072
+ );
2073
+ releasePreparedWorkOwnership(ctx, current);
2074
+ updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
2075
+ activeWorkId: undefined,
2076
+ activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
2077
+ });
2078
+ return {
2079
+ work: compactHandle(current),
2080
+ stages: current.finalization,
2081
+ completed: true,
2082
+ cleanupCompleted: false,
2083
+ cleanupRetained: true,
2084
+ cleanupPending: false,
2085
+ idempotent: true,
2086
+ };
2087
+ }
1525
2088
  if (current.state === 'cleaned') {
1526
2089
  const terminalContract = contractFor(ctx, current);
1527
2090
  if (
@@ -1554,7 +2117,18 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1554
2117
  if (requestedOutcome === 'completed_no_change' && (!noChangeProof || args.commit === true || args.merge === true)) {
1555
2118
  throw new Error('WORK_NO_CHANGE_PROOF_REQUIRED: completed_no_change requires objective-specific evidence and forbids commit/merge');
1556
2119
  }
1557
- const wants = { commit: args.commit === true, merge: args.merge === true, cleanup: args.cleanup === true };
2120
+ const wants = requestedWants;
2121
+ const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
2122
+ ? args.target_branch.trim()
2123
+ : undefined;
2124
+ // With merge=false an explicitly selected Work branch is the delivery target,
2125
+ // not a disposable feature branch. Keep the ref reachable for the receipt.
2126
+ const retainExplicitWorkTargetBranch = !wants.merge && explicitTargetBranch === current.branch;
2127
+ const deleteBranchRequested = args.delete_branch !== false && !retainExplicitWorkTargetBranch;
2128
+ const noChangeFastPath = requestedOutcome === 'completed_no_change'
2129
+ && !wants.commit
2130
+ && !wants.merge
2131
+ && current.state !== 'failed';
1558
2132
 
1559
2133
  const transact = (label: string, update: (fresh: WorkHandleState) => WorkHandleState): WorkHandleState =>
1560
2134
  withControllerLock(ctx.controllerHome, { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId }, `work-finalize:${current.workId}:${label}`, () => {
@@ -1567,15 +2141,31 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1567
2141
  const finalization = { ...fresh.finalization, [stage]: 'failed', lastError: reason } as WorkFinalizationStages;
1568
2142
  return markWorkHandleFailed(ctx.controllerHome, { ...fresh, finalization }, reason);
1569
2143
  });
1570
- if (current.workContractId) updateWorkContract({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId, { status: 'failed' });
1571
- markRepositoryProjectionDirty(ctx.controllerHome, current.repositoryId, `cleanup:${current.workId}:failed`);
2144
+ if (current.workContractId) {
2145
+ if (stage === 'validation') {
2146
+ updateWorkContract({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId, { status: 'failed' });
2147
+ } else if (stage === 'commit' || stage === 'merge' || stage === 'branchCleanup' || stage === 'worktreeCleanup') {
2148
+ transitionWorkContractPhase(
2149
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
2150
+ current.workContractId,
2151
+ {
2152
+ phase: retryPhaseForFinalizationStage(stage),
2153
+ status: 'blocked',
2154
+ state: 'blocked',
2155
+ summary: `Retryable Work finalization stage ${stage} failed: ${reason}`,
2156
+ },
2157
+ );
2158
+ }
2159
+ }
2160
+ markRepositoryProjectionDirty(ctx.controllerHome, current.repositoryId, `finalize:${current.workId}:${String(stage)}-failed`);
1572
2161
  return { work: compactHandle(current), stages: current.finalization, completed: false };
1573
2162
  };
1574
2163
 
1575
2164
  const failedCleanupRequested = current.state === 'failed'
1576
2165
  && wants.cleanup
1577
2166
  && !wants.commit
1578
- && !wants.merge;
2167
+ && !wants.merge
2168
+ && retryStage === undefined;
1579
2169
  const failedCleanupProof = failedCleanupRequested
1580
2170
  ? failedCleanupOnlyHead(ctx, current, args)
1581
2171
  : undefined;
@@ -1583,12 +2173,17 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1583
2173
  throw new Error('WORK_FAILED_CLEANUP_UNSAFE: failed Work cleanup requires exact checkout/branch ownership and an unchanged clean controller-owned managed worktree whose exact HEAD is already contained in the target branch');
1584
2174
  }
1585
2175
 
1586
- if (!failedCleanupRequested) {
2176
+ if (!failedCleanupRequested && !noChangeFastPath) {
1587
2177
  current = transact('begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
1588
2178
  ...fresh,
1589
- state: fresh.state === 'failed' ? 'validating' : fresh.state,
2179
+ state: fresh.state === 'failed' ? retryHandleStateForFinalization(fresh.finalization) : fresh.state,
1590
2180
  failureReason: undefined,
1591
- finalization: resetFailedFinalizationStages(fresh.finalization, wants),
2181
+ finalization: resetFinalizationStagesForRequest(fresh.finalization, wants, {
2182
+ managedWorktree: fresh.managedWorktree,
2183
+ deleteBranchRequested,
2184
+ retainedByRequest: fresh.terminalResourceDisposition?.mode === 'retained_by_request',
2185
+ }),
2186
+ ...(wants.cleanup && fresh.terminalResourceDisposition?.mode === 'retained_by_request' ? { terminalResourceDisposition: undefined } : {}),
1592
2187
  }));
1593
2188
  }
1594
2189
 
@@ -1627,7 +2222,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1627
2222
  validation: 'failed',
1628
2223
  commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1629
2224
  merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
1630
- branchCleanup: args.delete_branch === false ? 'skipped' : fresh.finalization.branchCleanup,
2225
+ branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
1631
2226
  lastError: preservedFailure,
1632
2227
  },
1633
2228
  }));
@@ -1659,7 +2254,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1659
2254
  });
1660
2255
  }
1661
2256
 
1662
- if (args.delete_branch !== false && current.finalization.branchCleanup !== 'done') {
2257
+ if (deleteBranchRequested && current.finalization.branchCleanup !== 'done') {
1663
2258
  const target = selectWorkFinalizationTarget(
1664
2259
  getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
1665
2260
  current,
@@ -1713,14 +2308,14 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1713
2308
  validation: 'failed',
1714
2309
  commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1715
2310
  merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
1716
- branchCleanup: args.delete_branch === false ? 'skipped' : fresh.finalization.branchCleanup,
2311
+ branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
1717
2312
  worktreeCleanup: 'done',
1718
2313
  lastError: preservedFailure,
1719
2314
  },
1720
2315
  }));
1721
2316
  appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1722
2317
  title: 'failed work cleanup completed',
1723
- summary: `Controller preserved the failed Work outcome while removing its unchanged clean managed worktree and ${args.delete_branch === false ? 'retaining' : 'removing'} the local branch after proving ${failedCleanupProof.currentHead} is contained in ${failedCleanupProof.targetBranch}.`,
2318
+ summary: `Controller preserved the failed Work outcome while removing its unchanged clean managed worktree and ${deleteBranchRequested ? 'removing' : 'retaining'} the local branch after proving ${failedCleanupProof.currentHead} is contained in ${failedCleanupProof.targetBranch}.`,
1724
2319
  detailLevel: 'summary',
1725
2320
  });
1726
2321
  releasePreparedWorkOwnership(ctx, current);
@@ -1743,7 +2338,9 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1743
2338
  && wants.cleanup
1744
2339
  && !wants.commit
1745
2340
  && !wants.merge;
1746
- const cleanupReconciliation = cleanupOnlyMergedHead(ctx, current, args);
2341
+ const cleanupReconciliation = requestedOutcome === 'completed_no_change'
2342
+ ? undefined
2343
+ : cleanupOnlyMergedHead(ctx, current, args);
1747
2344
  if (cancelledCleanupRequested && !cleanupReconciliation) {
1748
2345
  throw new Error('WORK_CANCELLED_CLEANUP_UNSAFE: cancelled Work cleanup requires an unchanged clean managed worktree (or a previously recorded cleanup) and a branch HEAD already contained in the target branch');
1749
2346
  }
@@ -1758,13 +2355,24 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1758
2355
  validation: cleanupReconciliation.worktreeMissing ? 'done' : fresh.finalization.validation,
1759
2356
  commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1760
2357
  merge: 'done',
1761
- branchCleanup: args.delete_branch === false ? 'skipped' : 'pending',
2358
+ branchCleanup: deleteBranchRequested ? 'pending' : 'skipped',
1762
2359
  lastError: undefined,
1763
2360
  },
1764
2361
  }));
1765
2362
  }
1766
2363
 
2364
+ const changedCleanupOnly = wants.cleanup
2365
+ && !wants.commit
2366
+ && !wants.merge
2367
+ && requestedOutcome !== 'completed_no_change'
2368
+ && current.state !== 'merged'
2369
+ && current.state !== 'cleaned';
2370
+ if (changedCleanupOnly && !cleanupReconciliation) {
2371
+ throw new Error('WORK_CLEANUP_DELIVERY_NOT_PROVEN: cleanup-only completion requires the exact Work branch HEAD to already be contained in the target branch before any managed worktree is removed');
2372
+ }
2373
+
1767
2374
  const terminalCleanupOnly = Boolean(cleanupReconciliation && wants.cleanup && !wants.commit && !wants.merge);
2375
+ let exactValidationInput: ReturnType<typeof currentWorkValidationInput> | undefined;
1768
2376
  if (terminalCleanupOnly) {
1769
2377
  current = transact('terminal-cleanup-validation-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
1770
2378
  ...fresh,
@@ -1780,12 +2388,15 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1780
2388
  const validationContract = contractFor(ctx, current);
1781
2389
  if (!validationContract) throw new Error(`WORK_VALIDATION_CONTRACT_MISSING: ${current.workContractId ?? current.workId}`);
1782
2390
  const validationInput = currentWorkValidationInput(validatedRepository, current, validationContract.checks);
2391
+ exactValidationInput = validationInput;
1783
2392
  if (validationContract.checks.length === 0) {
1784
- current = transact('validation-no-checks', (fresh) => writeWorkHandle(ctx.controllerHome, {
1785
- ...fresh,
1786
- validatedInputFingerprint: validationInput.fingerprint,
1787
- finalization: { ...fresh.finalization, validation: 'done', lastError: undefined },
1788
- }));
2393
+ if (!noChangeFastPath) {
2394
+ current = transact('validation-no-checks', (fresh) => writeWorkHandle(ctx.controllerHome, {
2395
+ ...fresh,
2396
+ validatedInputFingerprint: validationInput.fingerprint,
2397
+ finalization: { ...fresh.finalization, validation: 'done', lastError: undefined },
2398
+ }));
2399
+ }
1789
2400
  projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required.');
1790
2401
  } else if (!hasCurrentWorkValidationAuthority({
1791
2402
  finalizationValidation: current.finalization.validation,
@@ -1801,16 +2412,52 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1801
2412
  markWorkValidationPending(ctx.controllerHome, current);
1802
2413
  throw new Error('WORK_VALIDATION_REQUIRED: run work_validate against the exact current workspace before finalization');
1803
2414
  }
1804
- if (wants.merge && !wants.commit && current.finalization.commit !== 'done' && !validationInput.clean) {
2415
+ if (!validationInput.clean && wants.commit && current.finalization.commit === 'done') {
2416
+ current = transact('rearm-delivery-after-validated-repair', (fresh) => writeWorkHandle(ctx.controllerHome, {
2417
+ ...fresh,
2418
+ state: 'validating',
2419
+ failureReason: undefined,
2420
+ finalization: resetFinalizationStagesForRequest(fresh.finalization, wants, {
2421
+ managedWorktree: fresh.managedWorktree,
2422
+ deleteBranchRequested,
2423
+ retainedByRequest: fresh.terminalResourceDisposition?.mode === 'retained_by_request',
2424
+ workspaceDirty: true,
2425
+ }),
2426
+ }));
2427
+ }
2428
+ if (wants.merge && !wants.commit && !validationInput.clean) {
1805
2429
  throw new Error('WORK_MERGE_UNCOMMITTED_CHANGES: commit the validated workspace before merging');
1806
2430
  }
1807
2431
  }
1808
2432
 
1809
- if (requestedOutcome === 'completed_no_change') {
1810
- const inspected = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
1811
- if (!repositoryGitStatus(inspected.worktreeRepository).clean) {
2433
+ if (noChangeFastPath) {
2434
+ if (!exactValidationInput?.clean) {
1812
2435
  throw new Error('WORK_NO_CHANGE_DIRTY: completed_no_change cannot retain an owned dirty worktree');
1813
2436
  }
2437
+ const noChangeFinalization: WorkFinalizationStages = {
2438
+ ...current.finalization,
2439
+ validation: 'done',
2440
+ commit: 'skipped',
2441
+ merge: 'skipped',
2442
+ branchCleanup: wants.cleanup && deleteBranchRequested ? 'pending' : 'skipped',
2443
+ lastError: undefined,
2444
+ };
2445
+ // Collapse the no-op Git stages into the same durable lifecycle write used
2446
+ // to enter/establish the delivery boundary. Physical cleanup remains
2447
+ // separately persisted because it has real side effects and crash semantics.
2448
+ if (current.state === 'prepared') {
2449
+ current = transact('no-change-validated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'validating', {
2450
+ finalization: noChangeFinalization,
2451
+ validatedInputFingerprint: exactValidationInput.fingerprint,
2452
+ failureReason: undefined,
2453
+ }));
2454
+ } else if (current.state === 'editing' || current.state === 'validating' || current.state === 'committed') {
2455
+ current = transact('no-change-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
2456
+ finalization: noChangeFinalization,
2457
+ validatedInputFingerprint: exactValidationInput.fingerprint,
2458
+ failureReason: undefined,
2459
+ }));
2460
+ }
1814
2461
  }
1815
2462
 
1816
2463
  if (wants.commit && current.finalization.commit === 'pending') {
@@ -1818,58 +2465,454 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1818
2465
  const contract = contractFor(ctx, current);
1819
2466
  if (contract?.constraints.allowCommit === false) throw new Error('WORK_COMMIT_NOT_ALLOWED: WorkContract disallows commit');
1820
2467
  const status = repositoryGitStatus(validated.worktreeRepository);
1821
- const commitPaths = [...new Set([...status.staged, ...status.unstaged, ...status.untracked])];
2468
+ const { dirtyPaths, ownedPaths: commitPaths } = workOwnedDirtyPaths(contract, status);
2469
+ if (dirtyPaths.length > 0 && commitPaths.length === 0) {
2470
+ throw new Error(`WORK_COMMIT_NO_OWNED_DIRTY_PATHS: ${current.workId} has ${dirtyPaths.length} dirty path(s), but none are owned by its WorkContract path scope`);
2471
+ }
1822
2472
  const committed = repositoryGitCommit(ctx.controllerHome, validated.worktreeRepository, { message: String(args.message ?? `Complete ${current.workId}`), paths: commitPaths, allowEmpty: false, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
1823
2473
  const pendingAuthorization = [committed.stage, committed.commit].find((execution) => execution?.authorizationDecision?.decision === 'user_confirmation_required')?.authorizationDecision;
1824
2474
  if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
1825
2475
  if (!committed.committed) return { ...failStage('commit', committed.error?.message ?? 'commit failed'), commit: committed };
2476
+ const checks = contract?.checks ?? [];
2477
+ const committedValidatedWorkspaceFingerprint = exactValidationInput && committed.after.clean
2478
+ ? workspaceValidationFingerprint(validated.worktreeRepository.canonicalRoot, committed.before)
2479
+ : undefined;
2480
+ const validationPreservedAcrossCommit = Boolean(
2481
+ checks.length > 0
2482
+ && exactValidationInput
2483
+ && committedValidatedWorkspaceFingerprint === exactValidationInput.workspaceFingerprint
2484
+ );
2485
+ const committedHead = gitHead(validated.worktreeRepository.canonicalRoot);
2486
+ const postCommitInput = currentWorkValidationInput(validated.worktreeRepository, { ...current, expectedHead: committedHead }, checks);
1826
2487
  current = transact('commit-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'committed', {
1827
- expectedHead: gitHead(validated.worktreeRepository.canonicalRoot),
2488
+ expectedHead: committedHead,
1828
2489
  finalization: {
1829
2490
  ...fresh.finalization,
1830
- validation: contract?.checks.length ? 'pending' : 'done',
2491
+ validation: checks.length ? (validationPreservedAcrossCommit ? 'done' : 'pending') : 'done',
1831
2492
  commit: 'done',
1832
2493
  lastError: undefined,
1833
2494
  },
1834
- validatedInputFingerprint: contract?.checks.length ? undefined : fresh.validatedInputFingerprint,
2495
+ validatedInputFingerprint: checks.length
2496
+ ? (validationPreservedAcrossCommit ? postCommitInput.fingerprint : undefined)
2497
+ : postCommitInput.fingerprint,
1835
2498
  failureReason: undefined,
1836
2499
  }));
1837
- if (contract?.checks.length) {
2500
+ if (checks.length && !validationPreservedAcrossCommit) {
1838
2501
  markWorkValidationPending(ctx.controllerHome, current);
1839
2502
  return {
1840
2503
  work: compactHandle(current),
1841
2504
  stages: current.finalization,
1842
2505
  completed: false,
1843
- continuation: 'WORK_COMMITTED_REVALIDATION_REQUIRED: run work_validate on the exact committed HEAD before merge or completion',
2506
+ continuation: 'WORK_COMMITTED_REVALIDATION_REQUIRED: committed content changed from the exact validated workspace; run work_validate on the committed HEAD before merge or completion',
1844
2507
  };
1845
2508
  }
1846
- const postCommitInput = currentWorkValidationInput(validated.worktreeRepository, current, []);
1847
- current = transact('commit-no-checks-validation', (fresh) => writeWorkHandle(ctx.controllerHome, {
1848
- ...fresh,
1849
- expectedHead: postCommitInput.head,
1850
- validatedInputFingerprint: postCommitInput.fingerprint,
1851
- }));
1852
- projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required after commit.');
2509
+ if (validationPreservedAcrossCommit && exactValidationInput) {
2510
+ const checkDefinitions = listControllerChecks(validated.worktreeRepository.canonicalRoot);
2511
+ for (const checkId of checks) {
2512
+ const check = checkDefinitions.find((entry) => entry.id === checkId);
2513
+ if (check?.effects?.git !== undefined) continue;
2514
+ const sourcePass = contract
2515
+ ? effectiveVerificationEvidence(contract.checkRefs, {
2516
+ sourceRevision: exactValidationInput.head,
2517
+ workspaceFingerprint: exactValidationInput.workspaceFingerprint,
2518
+ checkId,
2519
+ requestedChecks: checks,
2520
+ }).find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))?.record
2521
+ : undefined;
2522
+ if (!sourcePass) continue;
2523
+ appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
2524
+ ...sourcePass,
2525
+ summary: `Validation authority transferred across content-equivalent commit for ${checkId}.`,
2526
+ recordedAt: new Date().toISOString(),
2527
+ sourceRevision: postCommitInput.head,
2528
+ workspaceFingerprint: postCommitInput.workspaceFingerprint,
2529
+ verificationInputFingerprint: verificationInputFingerprint({
2530
+ sourceRevision: postCommitInput.head,
2531
+ workspaceFingerprint: postCommitInput.workspaceFingerprint,
2532
+ checkId,
2533
+ requestedChecks: checks,
2534
+ }),
2535
+ evidenceRef: {
2536
+ title: checkId,
2537
+ summary: 'Reused prior valid receipt after proving the validated filesystem content was committed without drift.',
2538
+ detailLevel: 'summary',
2539
+ },
2540
+ });
2541
+ }
2542
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
2543
+ title: 'validation authority preserved across content-equivalent commit',
2544
+ summary: `Exact validated workspace ${exactValidationInput.workspaceFingerprint.slice(0, 16)} was committed without content/status drift; non-Git-sensitive per-check authority transferred to committed HEAD ${postCommitInput.head}.`,
2545
+ detailLevel: 'summary',
2546
+ });
2547
+ } else if (checks.length === 0) {
2548
+ projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required after commit.');
2549
+ }
1853
2550
  } else if (!wants.commit && current.finalization.commit === 'pending') {
1854
2551
  current = transact('commit-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, commit: 'skipped' } }));
1855
2552
  }
1856
2553
 
2554
+ if (wants.merge && current.finalization.merge === 'pending' && current.finalization.commit === 'done') {
2555
+ const repository = getRepository(current.repositoryId, ctx.controllerHome);
2556
+ const target = selectWorkFinalizationTarget(repository, current);
2557
+ const targetBranch = explicitTargetBranch ?? repository.defaultBranch ?? 'main';
2558
+ const directTarget = inspectDirectTargetDelivery(
2559
+ target.canonicalRoot,
2560
+ current.worktreePath,
2561
+ current.managedWorktree,
2562
+ current.branch,
2563
+ targetBranch,
2564
+ current.expectedHead,
2565
+ );
2566
+ if (directTarget.integrated) {
2567
+ current = transact('direct-target-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
2568
+ finalization: { ...fresh.finalization, merge: 'done', branchCleanup: 'skipped', lastError: undefined },
2569
+ failureReason: undefined,
2570
+ }));
2571
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
2572
+ title: 'direct target delivery already integrated',
2573
+ summary: `Work ${current.workId} runs directly on ${targetBranch}; exact delivery revision ${directTarget.expectedHead} is reachable from current target ${directTarget.targetHead}, so no self-merge or target-branch deletion is required.`,
2574
+ detailLevel: 'summary',
2575
+ });
2576
+ }
2577
+ }
2578
+
1857
2579
  if (wants.merge && current.finalization.merge === 'pending') {
1858
- validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
2580
+ const mergeValidated = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
2581
+ if (mergeValidated.currentHead && mergeValidated.currentHead !== current.expectedHead) {
2582
+ current = transact('merge-adopt-work-delivery-head', (fresh) => writeWorkHandle(ctx.controllerHome, {
2583
+ ...fresh,
2584
+ expectedHead: mergeValidated.currentHead,
2585
+ failureReason: undefined,
2586
+ }));
2587
+ }
1859
2588
  const contract = contractFor(ctx, current);
1860
2589
  if (contract?.constraints.allowMerge === false) throw new Error('WORK_MERGE_NOT_ALLOWED: WorkContract disallows merge');
1861
2590
  const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
1862
- const deleteAfterWorktreeCleanup = current.managedWorktree && args.delete_branch !== false;
1863
- const merged = repositoryGitFinishWorkflow(ctx.controllerHome, target, { featureBranch: current.branch, targetBranch: typeof args.target_branch === 'string' ? args.target_branch : undefined, deleteBranch: !deleteAfterWorktreeCleanup && args.delete_branch !== false, noFf: args.no_ff === true, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
2591
+ const deleteAfterWorktreeCleanup = current.managedWorktree && deleteBranchRequested;
2592
+ const targetBranch = explicitTargetBranch ?? target.defaultBranch ?? 'main';
2593
+ const activeMergeHead = retryStage === 'merge' ? gitRevision(target.canonicalRoot, 'MERGE_HEAD') : undefined;
2594
+ if (!activeMergeHead && current.managedWorktree && current.expectedHead) {
2595
+ const targetHead = gitRevision(target.canonicalRoot, targetBranch);
2596
+ if (targetHead) {
2597
+ const advance = inspectWorkTargetAdvance(mergeValidated.worktreeRepository.canonicalRoot, current.expectedHead, targetHead);
2598
+ if (advance.relation === 'diverged_conflict') {
2599
+ if (contract) {
2600
+ transitionWorkContractPhase(
2601
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
2602
+ contract.workId,
2603
+ {
2604
+ phase: 'delivery',
2605
+ status: 'blocked',
2606
+ state: 'blocked',
2607
+ summary: `Target branch ${targetBranch} advanced to ${advance.targetHead} and conflicts with candidate ${advance.candidateHead}; canonical target was not mutated.`,
2608
+ },
2609
+ );
2610
+ }
2611
+ return {
2612
+ work: compactHandle(current),
2613
+ stages: current.finalization,
2614
+ completed: false,
2615
+ blocked: true,
2616
+ recoverable: true,
2617
+ error: {
2618
+ code: 'WORK_TARGET_ADVANCE_CONFLICT',
2619
+ message: `Target branch ${targetBranch} advanced and cannot be integrated cleanly into the isolated Work candidate. Canonical target was left unchanged.${advance.detail ? ` ${advance.detail}` : ''}`,
2620
+ },
2621
+ continuation: 'WORK_TARGET_ADVANCE_REVIEW_REQUIRED: inspect the target/candidate conflict, repair only inside the managed Work checkout, then revalidate before retrying finalize.',
2622
+ };
2623
+ }
2624
+ if (advance.relation === 'candidate_contains_target') {
2625
+ const nonLinearCommits = targetAdvanceLinearMergeCommits(
2626
+ mergeValidated.worktreeRepository.canonicalRoot,
2627
+ advance.targetHead,
2628
+ advance.candidateHead,
2629
+ );
2630
+ if (nonLinearCommits.length > 0) {
2631
+ return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_HISTORY_VIOLATION: candidate already contains merge commit(s) above reconciled target ${advance.targetHead}: ${nonLinearCommits.slice(0, 8).join(', ')}`);
2632
+ }
2633
+ const scopeViolation = targetAdvanceWorkScopeViolation(contract, advance.candidateChangedPaths);
2634
+ if (scopeViolation) {
2635
+ return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: Work-owned ${scopeViolation.kind} path ${scopeViolation.path}`);
2636
+ }
2637
+ if (contract) replaceTargetAdvanceScopeEvidence(ctx, contract, advance.candidateChangedPaths);
2638
+ current = transact('target-advance-provenance-confirmed', (fresh) => writeWorkHandle(ctx.controllerHome, {
2639
+ ...fresh,
2640
+ deliveryBaseCommit: advance.targetHead,
2641
+ }));
2642
+ }
2643
+ if (advance.relation === 'diverged_clean') {
2644
+ const preIntegrationScopeViolation = targetAdvanceWorkScopeViolation(contract, advance.candidateChangedPaths);
2645
+ if (preIntegrationScopeViolation) {
2646
+ return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: Work-owned ${preIntegrationScopeViolation.kind} path ${preIntegrationScopeViolation.path}`);
2647
+ }
2648
+ const checks = contract?.checks ?? [];
2649
+ const candidateInput = currentWorkValidationInput(mergeValidated.worktreeRepository, current, checks);
2650
+ const checksBefore = checks.length > 0 ? listControllerChecks(mergeValidated.worktreeRepository.canonicalRoot) : [];
2651
+ const integrated = repositoryGitRebaseOnto(ctx.controllerHome, mergeValidated.worktreeRepository, {
2652
+ onto: advance.targetHead,
2653
+ upstream: advance.mergeBase,
2654
+ abortOnFailure: true,
2655
+ authorizationDecision: gitAuthorization,
2656
+ sessionId: session.sessionId,
2657
+ principalId: session.principalId,
2658
+ workId: current.workId,
2659
+ goalId: current.goalId,
2660
+ });
2661
+ if (!integrated.rebased) {
2662
+ if (contract) {
2663
+ transitionWorkContractPhase(
2664
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
2665
+ contract.workId,
2666
+ {
2667
+ phase: 'delivery',
2668
+ status: 'blocked',
2669
+ state: 'blocked',
2670
+ summary: `Linear target-advance integration failed inside the isolated Work checkout; canonical ${targetBranch} was not mutated.`,
2671
+ },
2672
+ );
2673
+ }
2674
+ return {
2675
+ work: compactHandle(current),
2676
+ stages: current.finalization,
2677
+ completed: false,
2678
+ blocked: true,
2679
+ recoverable: integrated.restored,
2680
+ error: {
2681
+ code: 'WORK_TARGET_ADVANCE_LINEAR_INTEGRATION_FAILED',
2682
+ message: integrated.restored
2683
+ ? 'Target advancement could not be rebased safely; the isolated Work checkout was restored to its original candidate HEAD.'
2684
+ : 'Target advancement rebase failed and exact checkout restoration could not be proven; inspect the isolated Work checkout before retrying.',
2685
+ },
2686
+ continuation: 'WORK_TARGET_ADVANCE_REVIEW_REQUIRED: inspect the isolated Work checkout and retry only after its exact branch/HEAD/cleanliness are proven.',
2687
+ };
2688
+ }
2689
+ const integratedHead = gitHead(mergeValidated.worktreeRepository.canonicalRoot);
2690
+ if (!integratedHead || !gitIsAncestor(mergeValidated.worktreeRepository.canonicalRoot, advance.targetHead, integratedHead)) {
2691
+ return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_ANCESTRY_MISMATCH: target ${advance.targetHead} is not an ancestor of ${integratedHead ?? 'unavailable'}`);
2692
+ }
2693
+ const mergeCommits = targetAdvanceLinearMergeCommits(
2694
+ mergeValidated.worktreeRepository.canonicalRoot,
2695
+ advance.targetHead,
2696
+ integratedHead,
2697
+ );
2698
+ const integratedTree = spawnSync('git', ['-C', mergeValidated.worktreeRepository.canonicalRoot, 'rev-parse', '--verify', `${integratedHead}^{tree}`], {
2699
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
2700
+ });
2701
+ if (mergeCommits.length > 0) {
2702
+ return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_HISTORY_VIOLATION: integrated candidate contains merge commit(s): ${mergeCommits.slice(0, 8).join(', ')}`);
2703
+ }
2704
+ if (
2705
+ !advance.mergedTree
2706
+ || integratedTree.status !== 0
2707
+ || integratedTree.error
2708
+ || typeof integratedTree.stdout !== 'string'
2709
+ || integratedTree.stdout.trim() !== advance.mergedTree
2710
+ ) {
2711
+ return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_TREE_MISMATCH: rebased candidate tree does not equal the preflight clean integration tree ${advance.mergedTree ?? 'unavailable'}`);
2712
+ }
2713
+ const workOwnedChangedPaths = gitChangedPaths(mergeValidated.worktreeRepository.canonicalRoot, advance.targetHead, integratedHead);
2714
+ const postIntegrationScopeViolation = targetAdvanceWorkScopeViolation(contract, workOwnedChangedPaths);
2715
+ if (postIntegrationScopeViolation) {
2716
+ return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: integrated Work-owned ${postIntegrationScopeViolation.kind} path ${postIntegrationScopeViolation.path}`);
2717
+ }
2718
+ const integratedInput = currentWorkValidationInput(
2719
+ mergeValidated.worktreeRepository,
2720
+ { ...current, expectedHead: integratedHead },
2721
+ checks,
2722
+ );
2723
+ const checksAfter = checks.length > 0 ? listControllerChecks(mergeValidated.worktreeRepository.canonicalRoot) : [];
2724
+ const transferPlan = contract
2725
+ ? planTargetAdvanceValidationAuthority({
2726
+ checkIds: checks,
2727
+ checkRefs: contract.checkRefs,
2728
+ checksBefore,
2729
+ checksAfter,
2730
+ candidateHead: advance.candidateHead,
2731
+ candidateWorkspaceFingerprint: candidateInput.workspaceFingerprint,
2732
+ integratedHead,
2733
+ integratedWorkspaceFingerprint: integratedInput.workspaceFingerprint,
2734
+ targetChangedPaths: advance.targetChangedPaths,
2735
+ })
2736
+ : { transferredRecords: [], reusableCheckIds: [], invalidatedCheckIds: checks };
2737
+ for (const record of transferPlan.transferredRecords) {
2738
+ appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, record);
2739
+ }
2740
+ if (contract) replaceTargetAdvanceScopeEvidence(ctx, contract, workOwnedChangedPaths);
2741
+ const validationPreserved = transferPlan.invalidatedCheckIds.length === 0;
2742
+ current = transact('target-advance-integrated', (fresh) => transitionWorkHandle(
2743
+ ctx.controllerHome,
2744
+ fresh,
2745
+ validationPreserved ? 'committed' : 'validating',
2746
+ {
2747
+ deliveryBaseCommit: advance.targetHead,
2748
+ expectedHead: integratedHead,
2749
+ failureReason: undefined,
2750
+ finalization: { ...fresh.finalization, validation: validationPreserved ? 'done' : 'pending', merge: 'pending', lastError: undefined },
2751
+ validationRun: undefined,
2752
+ validatedInputFingerprint: validationPreserved ? integratedInput.fingerprint : undefined,
2753
+ },
2754
+ ));
2755
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
2756
+ title: 'target advancement linearly integrated into isolated Work candidate',
2757
+ summary: `Canonical ${targetBranch} at ${advance.targetHead} advanced independently. Forge rebased Work candidate ${advance.candidateHead} onto it as ${integratedHead} with no merge commits; Work scope is ${workOwnedChangedPaths.length} target-relative path(s). Validation authority transferred for ${transferPlan.reusableCheckIds.length}/${checks.length} check(s); ${transferPlan.invalidatedCheckIds.length} check(s) require fresh evidence.`,
2758
+ detailLevel: 'summary',
2759
+ });
2760
+ if (!validationPreserved) {
2761
+ markWorkValidationPending(ctx.controllerHome, current);
2762
+ return {
2763
+ work: compactHandle(current),
2764
+ stages: current.finalization,
2765
+ completed: false,
2766
+ continuation: `WORK_COMMITTED_REVALIDATION_REQUIRED: target branch ${targetBranch} changed inputs for [${transferPlan.invalidatedCheckIds.join(', ')}]; unaffected check evidence was transferred to ${integratedHead} and may be reused`,
2767
+ };
2768
+ }
2769
+ projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', checks.length === 0
2770
+ ? 'Target advancement was linearly integrated and no validation checks were required.'
2771
+ : `Target advancement was linearly integrated; all ${checks.length} check result(s) retained authority because target-only changes did not affect their declared inputs.`);
2772
+ }
2773
+ }
2774
+ }
2775
+ let merged: ReturnType<typeof repositoryGitFinishWorkflow>;
2776
+ if (activeMergeHead) {
2777
+ const expectedMergeHead = current.expectedHead ? gitRevision(target.canonicalRoot, current.expectedHead) : undefined;
2778
+ const featureHead = gitRevision(target.canonicalRoot, current.branch);
2779
+ const currentBranchResult = spawnSync('git', ['-C', target.canonicalRoot, 'branch', '--show-current'], {
2780
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
2781
+ });
2782
+ const currentBranchName = currentBranchResult.status === 0 && typeof currentBranchResult.stdout === 'string'
2783
+ ? currentBranchResult.stdout.trim()
2784
+ : '';
2785
+ if (!expectedMergeHead || activeMergeHead !== expectedMergeHead || featureHead !== expectedMergeHead || currentBranchName !== targetBranch) {
2786
+ return failStage(
2787
+ 'merge',
2788
+ `WORK_MERGE_HEAD_OWNERSHIP_MISMATCH: retry may conclude only Work ${current.workId} merge head ${expectedMergeHead ?? 'unavailable'} from ${current.branch} while on ${targetBranch}; observed MERGE_HEAD ${activeMergeHead}, feature ${featureHead ?? 'unavailable'}, branch ${currentBranchName || 'unavailable'}`,
2789
+ );
2790
+ }
2791
+ const unmerged = spawnSync('git', ['-C', target.canonicalRoot, 'diff', '--name-only', '--diff-filter=U'], {
2792
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
2793
+ });
2794
+ if (unmerged.status !== 0 || unmerged.error || typeof unmerged.stdout !== 'string') {
2795
+ return failStage('merge', 'WORK_MERGE_CONFLICT_STATE_UNAVAILABLE: unable to prove that the owned merge has no unmerged paths');
2796
+ }
2797
+ if (unmerged.stdout.trim()) {
2798
+ return failStage('merge', `WORK_MERGE_CONFLICTS_UNRESOLVED: resolve and stage all paths before retrying finalize (${unmerged.stdout.trim().split('\n').filter(Boolean).join(', ')})`);
2799
+ }
2800
+ const beforeConclude = repositoryGitStatus(target);
2801
+ const concluded = repositoryGitCommit(ctx.controllerHome, target, {
2802
+ message: `Merge ${current.branch} into ${targetBranch}`,
2803
+ allowEmpty: true,
2804
+ authorizationDecision: gitAuthorization,
2805
+ sessionId: session.sessionId,
2806
+ principalId: session.principalId,
2807
+ workId: current.workId,
2808
+ goalId: current.goalId,
2809
+ });
2810
+ if (!concluded.committed || !concluded.commit) {
2811
+ return failStage('merge', concluded.error?.message ?? 'WORK_MERGE_CONCLUDE_FAILED: git commit did not conclude the owned merge');
2812
+ }
2813
+ const concludedSecondParent = gitRevision(target.canonicalRoot, 'HEAD^2');
2814
+ if (concludedSecondParent !== expectedMergeHead || gitRevision(target.canonicalRoot, 'MERGE_HEAD')) {
2815
+ return failStage('merge', `WORK_MERGE_CONCLUSION_OWNERSHIP_MISMATCH: concluded merge must retain ${expectedMergeHead} as its second parent and clear MERGE_HEAD`);
2816
+ }
2817
+ const steps: ReturnType<typeof repositoryGitFinishWorkflow>['steps'] = [{ name: 'conclude_owned_merge', execution: concluded.commit }];
2818
+ if (!deleteAfterWorktreeCleanup && deleteBranchRequested) {
2819
+ const deleted = repositoryGitDeleteBranch(ctx.controllerHome, target, { branch: current.branch, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
2820
+ steps.push({ name: 'delete_feature_branch', execution: deleted.execution });
2821
+ if (deleted.execution.status !== 'executed' || deleted.execution.ok !== true) {
2822
+ return failStage('merge', deleted.execution.stderr || 'WORK_MERGE_BRANCH_CLEANUP_FAILED: owned merge concluded but feature branch deletion failed');
2823
+ }
2824
+ }
2825
+ merged = {
2826
+ repoId: target.repoId,
2827
+ checkoutId: target.activeCheckoutId,
2828
+ featureBranch: current.branch,
2829
+ targetBranch,
2830
+ before: beforeConclude,
2831
+ steps,
2832
+ after: repositoryGitStatus(target),
2833
+ completed: true,
2834
+ };
2835
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
2836
+ title: 'owned resolved merge concluded',
2837
+ summary: `Finalizer retry proved MERGE_HEAD ${activeMergeHead} belongs to the exact Work delivery head, found no unmerged paths, and concluded the existing merge instead of replaying git merge.`,
2838
+ detailLevel: 'summary',
2839
+ });
2840
+ } else {
2841
+ merged = repositoryGitFinishWorkflow(ctx.controllerHome, target, { featureBranch: current.branch, targetBranch: explicitTargetBranch, deleteBranch: !deleteAfterWorktreeCleanup && deleteBranchRequested, noFf: args.no_ff === true, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
2842
+ }
1864
2843
  const pendingAuthorization = merged.steps.find((step) => step.execution.authorizationDecision?.decision === 'user_confirmation_required')?.execution.authorizationDecision;
1865
2844
  if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
1866
2845
  if (!merged.completed) return { ...failStage('merge', merged.error?.message ?? 'merge failed'), merge: merged };
1867
2846
  current = transact('merge-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
1868
- finalization: { ...fresh.finalization, merge: 'done', branchCleanup: args.delete_branch === false ? 'skipped' : deleteAfterWorktreeCleanup ? 'pending' : 'done', lastError: undefined },
2847
+ finalization: { ...fresh.finalization, merge: 'done', branchCleanup: !deleteBranchRequested ? 'skipped' : deleteAfterWorktreeCleanup ? 'pending' : 'done', lastError: undefined },
1869
2848
  failureReason: undefined,
1870
2849
  }));
1871
2850
  } else if (!wants.merge && current.finalization.merge === 'pending') {
1872
- current = transact('merge-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, merge: 'skipped', branchCleanup: 'skipped' } }));
2851
+ current = transact('merge-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
2852
+ ...fresh,
2853
+ finalization: {
2854
+ ...fresh.finalization,
2855
+ merge: 'skipped',
2856
+ branchCleanup: requestedOutcome === 'completed_no_change' && wants.cleanup && deleteBranchRequested ? 'pending' : 'skipped',
2857
+ },
2858
+ }));
2859
+ }
2860
+
2861
+ let remoteDelivery: WorkRemoteDeliveryReceipt | undefined;
2862
+ const remoteDeliveryRequired = args.remote_write === true
2863
+ || contractFor(ctx, current)?.constraints.remoteDeliveryRequired === true;
2864
+ if (remoteDeliveryRequired && requestedOutcome !== 'completed_no_change') {
2865
+ const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
2866
+ const target = selectWorkFinalizationTarget(repository, current);
2867
+ const targetBranch = explicitTargetBranch ?? repository.defaultBranch ?? 'main';
2868
+ const targetRevision = gitRevision(target.canonicalRoot, targetBranch);
2869
+ if (!targetRevision) throw new Error(`WORK_REMOTE_DELIVERY_TARGET_REQUIRED: target branch ${targetBranch} is unavailable`);
2870
+ const authorityRepository = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
2871
+ remoteDelivery = await pushExactWorkRemoteDelivery({
2872
+ controllerHome: ctx.controllerHome,
2873
+ repository: authorityRepository,
2874
+ workId: current.workId,
2875
+ targetBranch,
2876
+ targetRevision,
2877
+ });
2878
+ appendWorkEvidence(
2879
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
2880
+ current.workContractId ?? current.workId,
2881
+ {
2882
+ title: 'remote delivery verified before Work terminalization',
2883
+ summary: remoteDelivery.pushed
2884
+ ? `Exact integrated revision ${remoteDelivery.targetRevision} was pushed to origin/${targetBranch} while Work ${current.workId} still held active delivery authority.`
2885
+ : `origin/${targetBranch} already contained exact integrated revision ${remoteDelivery.targetRevision}; remote delivery was verified idempotently before Work terminalization.`,
2886
+ detailLevel: 'summary',
2887
+ },
2888
+ );
2889
+ }
2890
+
2891
+ if (
2892
+ requestedOutcome === 'completed_no_change'
2893
+ && current.finalization.validation === 'done'
2894
+ && current.finalization.commit === 'skipped'
2895
+ && current.finalization.merge === 'skipped'
2896
+ && current.state !== 'merged'
2897
+ && current.state !== 'cleaned'
2898
+ ) {
2899
+ // `merged` is the WorkHandle's durable integrated-delivery boundary. A
2900
+ // no-change completion has no Git merge, but it must cross that boundary
2901
+ // before physical cleanup so an earlier failure preserves retryability.
2902
+ if (current.state === 'prepared') {
2903
+ current = transact('no-change-validation-state', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'validating', {
2904
+ finalization: fresh.finalization,
2905
+ failureReason: undefined,
2906
+ }));
2907
+ }
2908
+ if (current.state === 'editing' || current.state === 'validating' || current.state === 'committed') {
2909
+ current = transact('no-change-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
2910
+ finalization: fresh.finalization,
2911
+ failureReason: undefined,
2912
+ }));
2913
+ } else if (current.state !== 'merged') {
2914
+ throw new Error(`WORK_NO_CHANGE_DELIVERY_STATE_INVALID: cannot establish no-change delivery from ${current.state}`);
2915
+ }
1873
2916
  }
1874
2917
 
1875
2918
  if (wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
@@ -1891,7 +2934,11 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1891
2934
  current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
1892
2935
  }
1893
2936
 
1894
- if (wants.cleanup && current.finalization.branchCleanup === 'pending' && current.finalization.merge === 'done') {
2937
+ if (
2938
+ wants.cleanup
2939
+ && current.finalization.branchCleanup === 'pending'
2940
+ && (current.finalization.merge === 'done' || requestedOutcome === 'completed_no_change')
2941
+ ) {
1895
2942
  const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
1896
2943
  const deleted = repositoryGitDeleteBranch(ctx.controllerHome, target, { branch: current.branch, force: false, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
1897
2944
  if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
@@ -1929,18 +2976,64 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1929
2976
  });
1930
2977
  }
1931
2978
  const receipt = completionReceiptForFinalizedWork(ctx, current, completionContract, args);
1932
- const recorded = recordWorkCompletionReceipt(
2979
+ const ownerPrincipal = terminalizationOwner.principalId?.trim() || terminalizationOwner.controllerId;
2980
+ const ownerInstanceId = terminalizationOwner.controllerInstanceId?.trim() || '';
2981
+ const ownerClaimGeneration = terminalizationOwner.claimGeneration;
2982
+ if (!ownerInstanceId || typeof ownerClaimGeneration !== 'number' || ownerClaimGeneration < 1) {
2983
+ throw new Error(`WORK_CONTROLLER_TERMINALIZATION_AUTHORITY_INVALID: ${workId}`);
2984
+ }
2985
+ const fencedCompletion = withControllerSessionTerminalizationFence(
1933
2986
  { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1934
- workId,
1935
- receipt,
1936
- requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'completed_changed',
1937
- requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'repository_change',
2987
+ {
2988
+ workId,
2989
+ actor: `work-finalize-completion:${terminalizationOwner.controllerId}:${ownerInstanceId}`,
2990
+ authority: {
2991
+ controllerId: terminalizationOwner.controllerId,
2992
+ controllerType: terminalizationOwner.controllerType,
2993
+ principalId: ownerPrincipal,
2994
+ controllerInstanceId: ownerInstanceId,
2995
+ claimGeneration: ownerClaimGeneration,
2996
+ },
2997
+ },
2998
+ () => recordWorkCompletionReceipt(
2999
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
3000
+ workId,
3001
+ receipt,
3002
+ requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'completed_changed',
3003
+ requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'repository_change',
3004
+ ),
1938
3005
  );
3006
+ if (!fencedCompletion.allowed) {
3007
+ throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fencedCompletion.reason}`);
3008
+ }
3009
+ const recorded = fencedCompletion.value;
1939
3010
  if (recorded.requirementId) {
1940
- completeRequirementFromWork(
1941
- { controllerHome: ctx.controllerHome },
1942
- { requirementId: recorded.requirementId, work: recorded },
1943
- );
3011
+ try {
3012
+ completeRequirementFromWork(
3013
+ { controllerHome: ctx.controllerHome },
3014
+ { requirementId: recorded.requirementId, work: recorded },
3015
+ );
3016
+ } catch (error) {
3017
+ // Work completion + its receipt are the canonical machine-delivery facts.
3018
+ // Requirement completion is a downstream projection and may legitimately
3019
+ // be absent for callers that use requirement_id only as a semantic
3020
+ // identity. Never turn an already integrated/cleaned Work back into a
3021
+ // failed finalize because this projection is missing or stale.
3022
+ try {
3023
+ appendWorkEvidence(
3024
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
3025
+ workId,
3026
+ {
3027
+ title: 'requirement completion projection pending',
3028
+ summary: `Work completion remains authoritative; Requirement projection could not be applied: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2_000),
3029
+ detailLevel: 'summary',
3030
+ },
3031
+ );
3032
+ } catch {
3033
+ // Diagnostic persistence is also downstream of the completion
3034
+ // receipt and therefore cannot invalidate the terminal Work fact.
3035
+ }
3036
+ }
1944
3037
  }
1945
3038
  }
1946
3039
  // Successful WorkContract completion always ends controller ownership.
@@ -1952,7 +3045,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1952
3045
  activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
1953
3046
  });
1954
3047
  }
1955
- return { work: compactHandle(current), stages: current.finalization, completed: complete, idempotent: !wants.commit && !wants.merge && !wants.cleanup && current.finalization.validation === 'done' };
3048
+ return { work: compactHandle(current), stages: current.finalization, completed: complete, ...(remoteDelivery ? { remoteDelivery } : {}), idempotent: !wants.commit && !wants.merge && !wants.cleanup && current.finalization.validation === 'done' };
1956
3049
  }
1957
3050
 
1958
3051
  export async function callExecutionTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {