@moretea-labs/forge 1.6.0 → 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 (396) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -0
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +1 -1
  6. package/SKILL.md +2 -2
  7. package/THIRD_PARTY_NOTICES.md +1 -1
  8. package/agents/openai.yaml +1 -1
  9. package/assets/hooks/hook-input.sh +11 -8
  10. package/assets/hooks/lib/session-state.sh +10 -7
  11. package/assets/hooks/lib/workflow-state.sh +13 -177
  12. package/assets/hooks/post-tool-observer.sh +8 -2
  13. package/assets/hooks/prompt-guard.sh +5 -81
  14. package/assets/hooks/session-start-context.sh +56 -1
  15. package/assets/partials/05-workflow.partial.md +1 -1
  16. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  17. package/assets/plugin-registry.v1.json +1 -1
  18. package/assets/reference-configs/document-generation.md +1 -1
  19. package/assets/reference-configs/harness-overview.md +1 -1
  20. package/assets/reference-configs/sprint-contracts.md +3 -3
  21. package/assets/skill-version.json +2 -2
  22. package/assets/templates/contract.template.md +2 -7
  23. package/assets/templates/helpers/capture-plan.sh +2 -2
  24. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  25. package/assets/templates/helpers/contract-worktree.sh +1 -21
  26. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  27. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  28. package/assets/templates/helpers/new-plan.sh +1 -1
  29. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  30. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  31. package/assets/templates/helpers/verify-contract.sh +1 -111
  32. package/assets/templates/helpers/verify-sprint.sh +3 -180
  33. package/assets/templates/plan.template.md +1 -1
  34. package/assets/templates/review.template.md +9 -34
  35. package/assets/workflow-contract.v1.json +10 -19
  36. package/docs/README.md +10 -10
  37. package/docs/architecture/CURRENT.md +170 -0
  38. package/docs/architecture/EVOLUTION.md +42 -0
  39. package/docs/architecture/current/README.md +4 -156
  40. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  41. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  42. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  43. package/docs/architecture/history/README.md +7 -0
  44. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  45. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  46. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  47. package/docs/architecture/history.md +3 -34
  48. package/docs/architecture/index.md +15 -98
  49. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  50. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  51. package/docs/architecture/versions/1.6.md +38 -0
  52. package/docs/architecture/versions/1.7.md +41 -0
  53. package/docs/forge-chatgpt-browser-engine.md +2 -2
  54. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  55. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  56. package/docs/forge-github-issue-launcher.md +1 -1
  57. package/docs/forge-runtime-self-healing-loop.md +1 -1
  58. package/docs/operations/controller-browser-plugin.md +26 -6
  59. package/docs/operations/controller-performance-and-502.md +124 -0
  60. package/docs/operations/controller-reliability-runbook.md +1 -1
  61. package/docs/operations/features.md +2 -2
  62. package/docs/operations/github-repository.md +1 -1
  63. package/docs/operations/homebrew.md +2 -2
  64. package/docs/operations/mcp-tool-exposure.md +5 -3
  65. package/docs/wiki/Architecture.md +6 -6
  66. package/docs/wiki/Implementation.md +2 -2
  67. package/docs/wiki/Integrations.md +2 -2
  68. package/docs/wiki/Work-Lifecycle.md +1 -1
  69. package/package.json +29 -10
  70. package/scripts/activate-source-baseline.ts +17 -13
  71. package/scripts/benchmark-background-check-overlap.ts +228 -0
  72. package/scripts/benchmark-mcp-host.sh +117 -0
  73. package/scripts/benchmark-mcp-transport.ts +170 -0
  74. package/scripts/benchmark-quality-harness.ts +146 -0
  75. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  76. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  77. package/scripts/benchmark-typescript-navigation.ts +186 -0
  78. package/scripts/benchmark-work-efficiency.ts +334 -0
  79. package/scripts/capture-plan.sh +3 -3
  80. package/scripts/check-agent-tooling.sh +8 -35
  81. package/scripts/check-architecture-sync.sh +17 -86
  82. package/scripts/check-mcp-compatibility.ts +14 -0
  83. package/scripts/check-runtime-architecture.mjs +107 -64
  84. package/scripts/check-stable-baseline.ts +92 -0
  85. package/scripts/check-task-workflow.sh +4 -4
  86. package/scripts/contract-worktree.sh +1 -21
  87. package/scripts/ensure-task-workflow.sh +22 -55
  88. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  89. package/scripts/install-standalone-recovery.ts +24 -0
  90. package/scripts/lib/project-init-lib.sh +15 -44
  91. package/scripts/migrate-project-template.sh +23 -1179
  92. package/scripts/new-plan.sh +1 -1
  93. package/scripts/plan-to-todo.sh +11 -36
  94. package/scripts/run-governed-gate.ts +2 -1
  95. package/scripts/ship-worktrees.sh +2 -29
  96. package/scripts/smoke-ios-physical-device.ts +4 -2
  97. package/scripts/smoke-runtime-recovery.ts +1 -35
  98. package/scripts/smoke-schedule-engine.ts +1 -20
  99. package/scripts/verify-contract.sh +1 -111
  100. package/scripts/verify-forge-runtime.sh +0 -1
  101. package/scripts/verify-sprint.sh +3 -180
  102. package/scripts/workflow-contract.ts +1 -5
  103. package/src/cli/commands/init-hook.ts +3 -3
  104. package/src/cli/commands/mcp.ts +9 -5
  105. package/src/cli/commands/plugin.ts +1 -1
  106. package/src/cli/commands/recovery.ts +25 -3
  107. package/src/cli/commands/runtime.ts +55 -3
  108. package/src/cli/commands/security.ts +12 -1
  109. package/src/cli/commands/setup-profile.ts +31 -21
  110. package/src/cli/controller/check-runner.ts +106 -3
  111. package/src/cli/controller/composite-result.ts +1 -1
  112. package/src/cli/controller/context/focus.ts +51 -0
  113. package/src/cli/controller/context/instruction-context.ts +114 -0
  114. package/src/cli/controller/context/known-paths.ts +76 -0
  115. package/src/cli/controller/context/query-planning.ts +110 -0
  116. package/src/cli/controller/context/rule-resolution.ts +532 -0
  117. package/src/cli/controller/context/source-materializer.ts +278 -0
  118. package/src/cli/controller/context/structural.ts +16 -0
  119. package/src/cli/controller/context/types.ts +286 -0
  120. package/src/cli/controller/context-pack.ts +785 -469
  121. package/src/cli/controller/launch-agents.ts +77 -2
  122. package/src/cli/controller/operational-plan.ts +2 -2
  123. package/src/cli/controller/work-mode.ts +6 -4
  124. package/src/cli/editing/edit-session.ts +60 -7
  125. package/src/cli/github/github.ts +133 -0
  126. package/src/cli/installer/managed-entries.ts +15 -1
  127. package/src/cli/local-bridge/console-automations.ts +54 -78
  128. package/src/cli/local-bridge/console-view-models.ts +1 -35
  129. package/src/cli/local-bridge/facade-api.ts +0 -217
  130. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  131. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  132. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  133. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  134. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  135. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  136. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  137. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  138. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  139. package/src/cli/local-bridge/job-store.ts +1 -490
  140. package/src/cli/local-bridge/server.ts +0 -513
  141. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  142. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  143. package/src/cli/mcp/auth.ts +5 -5
  144. package/src/cli/mcp/instructions.ts +3 -2
  145. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  146. package/src/cli/mcp/multi-repository.ts +16 -5
  147. package/src/cli/mcp/oauth.ts +175 -26
  148. package/src/cli/mcp/repository-tools.ts +247 -107
  149. package/src/cli/mcp/server.ts +537 -111
  150. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  151. package/src/cli/mcp/toolset-selection.ts +1 -1
  152. package/src/cli/mcp/toolset.ts +42 -5
  153. package/src/cli/mcp/transports/http.ts +101 -14
  154. package/src/cli/mcp/transports/session-registry.ts +43 -5
  155. package/src/cli/mcp/types.ts +3 -3
  156. package/src/cli/repositories/command-classifier.ts +101 -19
  157. package/src/cli/repositories/command-executor.ts +55 -521
  158. package/src/cli/repositories/command-process.ts +108 -0
  159. package/src/cli/repositories/command-scope.ts +99 -41
  160. package/src/cli/repositories/controller-home.ts +7 -8
  161. package/src/cli/repositories/git-command-executor.ts +2 -0
  162. package/src/cli/repositories/locks.ts +53 -16
  163. package/src/cli/repositories/repository-snapshot.ts +121 -0
  164. package/src/cli/repositories/selected-path-actions.ts +17 -9
  165. package/src/cli/repositories/structured-git.ts +61 -3
  166. package/src/cli/repository/inspector.ts +553 -63
  167. package/src/cli/repository/session-cache.ts +58 -17
  168. package/src/cli/v81-entry.ts +1 -4
  169. package/src/effects/fs-transaction.ts +85 -17
  170. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  171. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  172. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  173. package/src/runtime/context/lsp-client.ts +283 -0
  174. package/src/runtime/context/semantic-navigation.ts +275 -0
  175. package/src/runtime/context/swift-navigation.ts +565 -0
  176. package/src/runtime/context/typescript-navigation.ts +226 -0
  177. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  178. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +47 -8
  179. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  180. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  181. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  182. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  183. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  184. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  185. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  186. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
  187. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  188. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
  189. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  190. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  191. package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
  192. package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
  193. package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
  194. package/src/runtime/control-plane/facade/index.ts +1 -0
  195. package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
  196. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  197. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  198. package/src/runtime/control-plane/facade/types.ts +22 -5
  199. package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
  200. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  201. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  202. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  203. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  204. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  205. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  206. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  207. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  208. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  209. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  210. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  211. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  212. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  213. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  214. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  215. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  216. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  217. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  218. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  219. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
  220. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  221. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  222. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  223. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  224. package/src/runtime/control-plane/release-retention.ts +62 -2
  225. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  226. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  227. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  228. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  229. package/src/runtime/diagnostics/performance.ts +24 -1
  230. package/src/runtime/evidence/event-ledger.ts +1 -1
  231. package/src/runtime/execution/jobs/store.ts +58 -4
  232. package/src/runtime/execution/jobs/types.ts +1 -1
  233. package/src/runtime/execution/managed-workspace.ts +203 -1
  234. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  235. package/src/runtime/execution/process-runtime/check-result.ts +53 -0
  236. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  237. package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
  238. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  239. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  240. package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
  241. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  242. package/src/runtime/execution/process-runtime/runtime.ts +43 -3
  243. package/src/runtime/execution/process-runtime/store.ts +1 -0
  244. package/src/runtime/execution/process-runtime/types.ts +2 -0
  245. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  246. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  247. package/src/runtime/execution/workers/executor.ts +0 -48
  248. package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
  249. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  250. package/src/runtime/gateway/mcp/router.ts +184 -82
  251. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  252. package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
  253. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  254. package/src/runtime/plugins/browser-adapter.ts +2901 -241
  255. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  256. package/src/runtime/plugins/browser-handoff.ts +92 -2
  257. package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
  258. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  259. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  260. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  261. package/src/runtime/plugins/browser-runtime.ts +183 -0
  262. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  263. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  264. package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
  265. package/src/runtime/plugins/external-adapter.ts +324 -3
  266. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  267. package/src/runtime/plugins/first-party-registry.ts +2 -0
  268. package/src/runtime/plugins/github-adapter.ts +125 -0
  269. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  270. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  271. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  272. package/src/runtime/plugins/ios-adapter.ts +106 -16
  273. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  274. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  275. package/src/runtime/plugins/lightweight-action.ts +153 -0
  276. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  277. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  278. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  279. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  280. package/src/runtime/plugins/resend-adapter.ts +1 -1
  281. package/src/runtime/plugins/store.ts +269 -29
  282. package/src/runtime/plugins/types.ts +35 -2
  283. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  284. package/src/runtime/projections/git-status-sampler.ts +16 -16
  285. package/src/runtime/projections/materialized-view.ts +28 -10
  286. package/src/runtime/recovery/maintenance-executor.ts +393 -28
  287. package/src/runtime/root/mcp-transport.ts +180 -10
  288. package/src/runtime/root/package-connector-service.ts +186 -8
  289. package/src/runtime/root/package-runtime-release.ts +143 -17
  290. package/src/runtime/root/package-runtime-service.ts +430 -22
  291. package/src/runtime/root/release-manifest.ts +7 -2
  292. package/src/runtime/root/release-materialize.ts +19 -0
  293. package/src/runtime/root/release-store.ts +48 -11
  294. package/src/runtime/root/runtime.ts +23 -5
  295. package/src/runtime/root/service.ts +100 -29
  296. package/src/runtime/root/types.ts +2 -0
  297. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  298. package/src/runtime/shared/trusted-executable.ts +62 -0
  299. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  300. package/src/runtime/standalone-recovery/core.ts +700 -92
  301. package/src/runtime/standalone-recovery/entry.ts +99 -23
  302. package/src/runtime/standalone-recovery/installer.ts +19 -3
  303. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  304. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  305. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  306. package/src/runtime/workflow/schedules/engine.ts +287 -151
  307. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  308. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  309. package/src/runtime/workflow/schedules/store.ts +131 -10
  310. package/src/runtime/workflow/schedules/types.ts +2 -4
  311. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  312. package/src/testing/test-governance.ts +8 -40
  313. package/assets/reference-configs/heartbeat-triage.md +0 -57
  314. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  315. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  316. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  317. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  318. package/docs/architecture/current/architecture-invariants.md +0 -331
  319. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  320. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  321. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  322. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  323. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  324. package/docs/architecture/current/entity-model.md +0 -612
  325. package/docs/architecture/current/external-plugin-governance.md +0 -396
  326. package/docs/architecture/current/failure-recovery.md +0 -447
  327. package/docs/architecture/current/governance.md +0 -213
  328. package/docs/architecture/current/human-interaction-plane.md +0 -87
  329. package/docs/architecture/current/implementation-status.md +0 -128
  330. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  331. package/docs/architecture/current/legacy-governance.md +0 -58
  332. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  333. package/docs/architecture/current/migration-roadmap.md +0 -105
  334. package/docs/architecture/current/multi-repository-execution.md +0 -371
  335. package/docs/architecture/current/operations-runbook.md +0 -45
  336. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  337. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  338. package/docs/architecture/current/repository-access-modes.md +0 -102
  339. package/docs/architecture/current/route-policy.md +0 -15
  340. package/docs/architecture/current/route-session-concurrency.md +0 -85
  341. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  342. package/docs/architecture/current/runtime-directory-map.md +0 -49
  343. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  344. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  345. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  346. package/docs/architecture/current/system-overview.md +0 -169
  347. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  348. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  349. package/docs/architecture/current/thin-harness-v1.md +0 -321
  350. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  351. package/docs/forge-autonomous-goal-loop.md +0 -9
  352. package/docs/forge-provider-configuration.md +0 -7
  353. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  354. package/docs/operations/gmail-assistant-routines.md +0 -186
  355. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  356. package/docs/operations/provider-configuration.md +0 -31
  357. package/scripts/harness-trace-grade.sh +0 -150
  358. package/scripts/heartbeat-triage.sh +0 -361
  359. package/scripts/maintenance-triage.sh +0 -167
  360. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  361. package/src/cli/repositories/goal-registry.ts +0 -239
  362. package/src/runtime/assistant/action-proposals.ts +0 -279
  363. package/src/runtime/assistant/intent.ts +0 -395
  364. package/src/runtime/assistant/model-provider.ts +0 -460
  365. package/src/runtime/assistant/openapi.ts +0 -223
  366. package/src/runtime/assistant/policy.ts +0 -89
  367. package/src/runtime/assistant/readiness.ts +0 -258
  368. package/src/runtime/assistant/routine-runtime.ts +0 -477
  369. package/src/runtime/assistant/schedule-binding.ts +0 -171
  370. package/src/runtime/assistant/standing-grants.ts +0 -375
  371. package/src/runtime/assistant/store.ts +0 -243
  372. package/src/runtime/assistant/types.ts +0 -135
  373. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  374. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  375. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  376. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  377. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  378. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  379. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  380. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  381. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  382. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  383. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  384. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  385. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  386. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  387. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  388. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  389. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  390. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  391. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  392. package/src/runtime/workflow/findings/store.ts +0 -204
  393. package/src/runtime/workflow/findings/types.ts +0 -58
  394. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  395. package/src/runtime/workflow/portfolio/store.ts +0 -185
  396. 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,6 +1029,7 @@ 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
1035
  // `failed` is a retryable execution-handle state (its transition table allows
@@ -776,6 +1149,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
776
1149
  const checks = boundedStringArray(args.checks, 30);
777
1150
  const baseRef = typeof args.base_ref === 'string' && args.base_ref.trim() ? args.base_ref.trim() : undefined;
778
1151
  const requestedCheckoutId = typeof args.checkout_id === 'string' && args.checkout_id.trim() ? args.checkout_id.trim() : undefined;
1152
+ const needsDependencies = args.needs_dependencies === true;
779
1153
  const baseCheckoutId = repository.activeCheckoutId;
780
1154
  const baseStatus = repositoryGitStatus(repository);
781
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');
@@ -791,6 +1165,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
791
1165
  allowedPaths,
792
1166
  checks,
793
1167
  baseRef,
1168
+ needsDependencies,
794
1169
  });
795
1170
 
796
1171
  return withWorkPrepareRequest({
@@ -876,7 +1251,12 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
876
1251
  });
877
1252
  try {
878
1253
  const workspace = useWorktree
879
- ? 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
+ })
880
1260
  : { mode: 'current' as const, checkoutId: baseCheckoutId, root: repository.canonicalRoot, branch: baseStatus.branch ?? 'detached', baseRevision: baseStatus.head ?? undefined, managed: false };
881
1261
  const refreshed = getRepository(repository.repoId, ctx.controllerHome);
882
1262
  const checkout = selectRepositoryCheckout(refreshed, workspace.checkoutId);
@@ -887,7 +1267,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
887
1267
  schemaVersion: 1, workId: createdWorkId, sessionId: session.sessionId, principalId: session.principalId,
888
1268
  repositoryId: repository.repoId, checkoutId: checkout.activeCheckoutId, worktreePath: checkout.canonicalRoot, branch,
889
1269
  sourceCheckoutId: baseCheckoutId, managedWorktree: workspace.managed, workContractId: contract.workId, goalId, delegationVersion: delegation.version,
890
- baseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
1270
+ baseCommit: workspace.baseRevision ?? head, deliveryBaseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
891
1271
  state: 'prepared', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), finalization: initialStage(),
892
1272
  cleanupResponsibility: { owner: 'work_finalizer', registeredAt: new Date().toISOString() },
893
1273
  };
@@ -1044,15 +1424,16 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
1044
1424
  maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : undefined,
1045
1425
  });
1046
1426
  const process = execution.process;
1047
- const completed = process?.completed === true || execution.route === 'process_direct';
1048
1427
  const ok = process ? process.ok === true : execution.ok === true;
1049
1428
  const status = process
1050
1429
  ? (process.completed
1051
1430
  ? (process.cancelled ? 'cancelled' : process.timedOut ? 'timed_out' : ok ? 'executed' : 'failed')
1052
1431
  : 'running')
1053
- : execution.route === 'durable'
1054
- ? 'deferred_durable'
1055
- : 'rejected';
1432
+ : execution.route === 'process_direct'
1433
+ ? (ok ? 'executed' : 'failed')
1434
+ : execution.route === 'durable'
1435
+ ? 'deferred_durable'
1436
+ : 'rejected';
1056
1437
  return {
1057
1438
  processId: process?.processId,
1058
1439
  commandId,
@@ -1086,8 +1467,9 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
1086
1467
  for (const [index, entry] of inputs.entries()) {
1087
1468
  const execution = await run(entry, index);
1088
1469
  ordered.push(execution);
1089
- // Do not launch another mutating command while this Work-owned Process
1090
- // 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.
1091
1473
  if (execution.process && !execution.process.completed) break;
1092
1474
  }
1093
1475
  return ordered;
@@ -1134,7 +1516,10 @@ export function selectDefaultWorkValidationChecks(
1134
1516
 
1135
1517
  async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
1136
1518
  const session = requireSession(ctx, args);
1137
- const handle = workForSession(ctx, session, args, { reconcileValidation: false });
1519
+ const handle = workForSession(ctx, session, args, {
1520
+ reconcileValidation: false,
1521
+ allowClaimedTerminalCleanup: args.cleanup !== false,
1522
+ });
1138
1523
  const terminalOutcome = terminalCleanupOutcome(ctx, handle);
1139
1524
  if (terminalOutcome && args.cleanup !== false) {
1140
1525
  return await reconcileTerminalCleanup(ctx, session, handle, args, terminalOutcome);
@@ -1186,6 +1571,24 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1186
1571
  checks.push({ checkId, ok: false, status: 'missing', summary: `Check not found: ${checkId}` });
1187
1572
  break;
1188
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
+ }
1189
1592
  const existingBinding = validationRun.processes[checkId];
1190
1593
  let process = existingBinding
1191
1594
  ? getCheckProcessHandle(ctx.controllerHome, handle.repositoryId, existingBinding.processId)
@@ -1226,11 +1629,16 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1226
1629
  break;
1227
1630
  }
1228
1631
  process = executed.process!;
1632
+ const launchedRecord = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
1229
1633
  validationRun = {
1230
1634
  ...validationRun,
1231
1635
  processes: {
1232
1636
  ...validationRun.processes,
1233
- [checkId]: { processId: process.processId, requestId: processRequestId },
1637
+ [checkId]: {
1638
+ processId: process.processId,
1639
+ requestId: processRequestId,
1640
+ ...(launchedRecord?.checkExecution ? { checkExecution: { ...launchedRecord.checkExecution } } : {}),
1641
+ },
1234
1642
  },
1235
1643
  };
1236
1644
  current = transitionWorkHandle(ctx.controllerHome, current, 'validating', { validationRun });
@@ -1244,16 +1652,32 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1244
1652
  checks.push({ checkId, ok: false, status: 'infrastructure_failure', summary: `Validation process record is unavailable: ${process.processId}` });
1245
1653
  break;
1246
1654
  }
1655
+ const boundCheckExecution = validationRun.processes[checkId]?.checkExecution;
1247
1656
  const receipt = processCheckCompletionReceipt(record, {
1248
1657
  repoId: handle.repositoryId,
1249
1658
  checkoutId: handle.checkoutId,
1250
1659
  workId: handle.workId,
1251
1660
  checkId,
1252
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
+ } : {}),
1253
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');
1254
1678
  appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, handle.workId, {
1255
1679
  checkId,
1256
- 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',
1257
1681
  summary: receipt.summary,
1258
1682
  recordedAt: receipt.finishedAt,
1259
1683
  sourceRevision: validationHead,
@@ -1263,7 +1687,6 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1263
1687
  workspaceFingerprint,
1264
1688
  checkId,
1265
1689
  requestedChecks,
1266
- commandId: receipt.commandId,
1267
1690
  }),
1268
1691
  commandFingerprint: commandFingerprint(checkId, receipt.commandId),
1269
1692
  resultArtifactId: receipt.receiptId,
@@ -1275,9 +1698,10 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1275
1698
  checks.push({
1276
1699
  checkId,
1277
1700
  ok: receipt.ok,
1278
- status: receipt.ok ? 'passed' : receipt.status === 'timed_out' || receipt.status === 'cancelled' ? 'infrastructure_failure' : 'failed',
1701
+ status: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'passed' : 'failed',
1279
1702
  process,
1280
1703
  receipt,
1704
+ ...(terminalEvidence.warning ? { warning: terminalEvidence.warning } : {}),
1281
1705
  });
1282
1706
  if (!receipt.ok) break;
1283
1707
  }
@@ -1405,7 +1829,6 @@ function cleanupOnlyMergedHead(
1405
1829
  encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
1406
1830
  });
1407
1831
  if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
1408
- if (current.expectedHead && currentHead === current.expectedHead && !cancelledContract) return undefined;
1409
1832
  if (current.expectedHead && currentHead !== current.expectedHead && cancelledContract) return undefined;
1410
1833
  return { currentHead, cancelledContract, worktreeMissing: false };
1411
1834
  }
@@ -1466,7 +1889,35 @@ function failedCleanupOnlyHead(
1466
1889
  return { currentHead: expectedHead, worktreeMissing: false, targetBranch };
1467
1890
  }
1468
1891
 
1469
- 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 {
1470
1921
  const next = { ...stages };
1471
1922
  let reset = false;
1472
1923
  if (next.validation === 'failed') {
@@ -1489,6 +1940,28 @@ function resetFailedFinalizationStages(stages: WorkFinalizationStages, wants: {
1489
1940
  next.branchCleanup = 'pending';
1490
1941
  reset = true;
1491
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
+ }
1492
1965
  if (reset) delete next.lastError;
1493
1966
  return next;
1494
1967
  }
@@ -1531,12 +2004,87 @@ function currentWorkValidationInput(
1531
2004
 
1532
2005
  async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
1533
2006
  const session = requireSession(ctx, args);
1534
- 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
+ }
1535
2030
  const terminalOutcome = terminalCleanupOutcome(ctx, current);
1536
2031
  if (terminalOutcome && args.cleanup !== false) {
1537
2032
  return await reconcileTerminalCleanup(ctx, session, current, args, terminalOutcome);
1538
2033
  }
1539
- 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
+ }
1540
2088
  if (current.state === 'cleaned') {
1541
2089
  const terminalContract = contractFor(ctx, current);
1542
2090
  if (
@@ -1569,7 +2117,18 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1569
2117
  if (requestedOutcome === 'completed_no_change' && (!noChangeProof || args.commit === true || args.merge === true)) {
1570
2118
  throw new Error('WORK_NO_CHANGE_PROOF_REQUIRED: completed_no_change requires objective-specific evidence and forbids commit/merge');
1571
2119
  }
1572
- 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';
1573
2132
 
1574
2133
  const transact = (label: string, update: (fresh: WorkHandleState) => WorkHandleState): WorkHandleState =>
1575
2134
  withControllerLock(ctx.controllerHome, { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId }, `work-finalize:${current.workId}:${label}`, () => {
@@ -1582,15 +2141,31 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1582
2141
  const finalization = { ...fresh.finalization, [stage]: 'failed', lastError: reason } as WorkFinalizationStages;
1583
2142
  return markWorkHandleFailed(ctx.controllerHome, { ...fresh, finalization }, reason);
1584
2143
  });
1585
- if (current.workContractId) updateWorkContract({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId, { status: 'failed' });
1586
- 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`);
1587
2161
  return { work: compactHandle(current), stages: current.finalization, completed: false };
1588
2162
  };
1589
2163
 
1590
2164
  const failedCleanupRequested = current.state === 'failed'
1591
2165
  && wants.cleanup
1592
2166
  && !wants.commit
1593
- && !wants.merge;
2167
+ && !wants.merge
2168
+ && retryStage === undefined;
1594
2169
  const failedCleanupProof = failedCleanupRequested
1595
2170
  ? failedCleanupOnlyHead(ctx, current, args)
1596
2171
  : undefined;
@@ -1598,12 +2173,17 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1598
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');
1599
2174
  }
1600
2175
 
1601
- if (!failedCleanupRequested) {
2176
+ if (!failedCleanupRequested && !noChangeFastPath) {
1602
2177
  current = transact('begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
1603
2178
  ...fresh,
1604
- state: fresh.state === 'failed' ? 'validating' : fresh.state,
2179
+ state: fresh.state === 'failed' ? retryHandleStateForFinalization(fresh.finalization) : fresh.state,
1605
2180
  failureReason: undefined,
1606
- 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 } : {}),
1607
2187
  }));
1608
2188
  }
1609
2189
 
@@ -1642,7 +2222,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1642
2222
  validation: 'failed',
1643
2223
  commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1644
2224
  merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
1645
- branchCleanup: args.delete_branch === false ? 'skipped' : fresh.finalization.branchCleanup,
2225
+ branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
1646
2226
  lastError: preservedFailure,
1647
2227
  },
1648
2228
  }));
@@ -1674,7 +2254,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1674
2254
  });
1675
2255
  }
1676
2256
 
1677
- if (args.delete_branch !== false && current.finalization.branchCleanup !== 'done') {
2257
+ if (deleteBranchRequested && current.finalization.branchCleanup !== 'done') {
1678
2258
  const target = selectWorkFinalizationTarget(
1679
2259
  getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
1680
2260
  current,
@@ -1728,14 +2308,14 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1728
2308
  validation: 'failed',
1729
2309
  commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1730
2310
  merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
1731
- branchCleanup: args.delete_branch === false ? 'skipped' : fresh.finalization.branchCleanup,
2311
+ branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
1732
2312
  worktreeCleanup: 'done',
1733
2313
  lastError: preservedFailure,
1734
2314
  },
1735
2315
  }));
1736
2316
  appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1737
2317
  title: 'failed work cleanup completed',
1738
- 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}.`,
1739
2319
  detailLevel: 'summary',
1740
2320
  });
1741
2321
  releasePreparedWorkOwnership(ctx, current);
@@ -1758,7 +2338,9 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1758
2338
  && wants.cleanup
1759
2339
  && !wants.commit
1760
2340
  && !wants.merge;
1761
- const cleanupReconciliation = cleanupOnlyMergedHead(ctx, current, args);
2341
+ const cleanupReconciliation = requestedOutcome === 'completed_no_change'
2342
+ ? undefined
2343
+ : cleanupOnlyMergedHead(ctx, current, args);
1762
2344
  if (cancelledCleanupRequested && !cleanupReconciliation) {
1763
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');
1764
2346
  }
@@ -1773,13 +2355,24 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1773
2355
  validation: cleanupReconciliation.worktreeMissing ? 'done' : fresh.finalization.validation,
1774
2356
  commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1775
2357
  merge: 'done',
1776
- branchCleanup: args.delete_branch === false ? 'skipped' : 'pending',
2358
+ branchCleanup: deleteBranchRequested ? 'pending' : 'skipped',
1777
2359
  lastError: undefined,
1778
2360
  },
1779
2361
  }));
1780
2362
  }
1781
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
+
1782
2374
  const terminalCleanupOnly = Boolean(cleanupReconciliation && wants.cleanup && !wants.commit && !wants.merge);
2375
+ let exactValidationInput: ReturnType<typeof currentWorkValidationInput> | undefined;
1783
2376
  if (terminalCleanupOnly) {
1784
2377
  current = transact('terminal-cleanup-validation-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
1785
2378
  ...fresh,
@@ -1795,12 +2388,15 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1795
2388
  const validationContract = contractFor(ctx, current);
1796
2389
  if (!validationContract) throw new Error(`WORK_VALIDATION_CONTRACT_MISSING: ${current.workContractId ?? current.workId}`);
1797
2390
  const validationInput = currentWorkValidationInput(validatedRepository, current, validationContract.checks);
2391
+ exactValidationInput = validationInput;
1798
2392
  if (validationContract.checks.length === 0) {
1799
- current = transact('validation-no-checks', (fresh) => writeWorkHandle(ctx.controllerHome, {
1800
- ...fresh,
1801
- validatedInputFingerprint: validationInput.fingerprint,
1802
- finalization: { ...fresh.finalization, validation: 'done', lastError: undefined },
1803
- }));
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
+ }
1804
2400
  projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required.');
1805
2401
  } else if (!hasCurrentWorkValidationAuthority({
1806
2402
  finalizationValidation: current.finalization.validation,
@@ -1816,16 +2412,52 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1816
2412
  markWorkValidationPending(ctx.controllerHome, current);
1817
2413
  throw new Error('WORK_VALIDATION_REQUIRED: run work_validate against the exact current workspace before finalization');
1818
2414
  }
1819
- 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) {
1820
2429
  throw new Error('WORK_MERGE_UNCOMMITTED_CHANGES: commit the validated workspace before merging');
1821
2430
  }
1822
2431
  }
1823
2432
 
1824
- if (requestedOutcome === 'completed_no_change') {
1825
- const inspected = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
1826
- if (!repositoryGitStatus(inspected.worktreeRepository).clean) {
2433
+ if (noChangeFastPath) {
2434
+ if (!exactValidationInput?.clean) {
1827
2435
  throw new Error('WORK_NO_CHANGE_DIRTY: completed_no_change cannot retain an owned dirty worktree');
1828
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
+ }
1829
2461
  }
1830
2462
 
1831
2463
  if (wants.commit && current.finalization.commit === 'pending') {
@@ -1833,58 +2465,454 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1833
2465
  const contract = contractFor(ctx, current);
1834
2466
  if (contract?.constraints.allowCommit === false) throw new Error('WORK_COMMIT_NOT_ALLOWED: WorkContract disallows commit');
1835
2467
  const status = repositoryGitStatus(validated.worktreeRepository);
1836
- 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
+ }
1837
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 });
1838
2473
  const pendingAuthorization = [committed.stage, committed.commit].find((execution) => execution?.authorizationDecision?.decision === 'user_confirmation_required')?.authorizationDecision;
1839
2474
  if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
1840
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);
1841
2487
  current = transact('commit-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'committed', {
1842
- expectedHead: gitHead(validated.worktreeRepository.canonicalRoot),
2488
+ expectedHead: committedHead,
1843
2489
  finalization: {
1844
2490
  ...fresh.finalization,
1845
- validation: contract?.checks.length ? 'pending' : 'done',
2491
+ validation: checks.length ? (validationPreservedAcrossCommit ? 'done' : 'pending') : 'done',
1846
2492
  commit: 'done',
1847
2493
  lastError: undefined,
1848
2494
  },
1849
- validatedInputFingerprint: contract?.checks.length ? undefined : fresh.validatedInputFingerprint,
2495
+ validatedInputFingerprint: checks.length
2496
+ ? (validationPreservedAcrossCommit ? postCommitInput.fingerprint : undefined)
2497
+ : postCommitInput.fingerprint,
1850
2498
  failureReason: undefined,
1851
2499
  }));
1852
- if (contract?.checks.length) {
2500
+ if (checks.length && !validationPreservedAcrossCommit) {
1853
2501
  markWorkValidationPending(ctx.controllerHome, current);
1854
2502
  return {
1855
2503
  work: compactHandle(current),
1856
2504
  stages: current.finalization,
1857
2505
  completed: false,
1858
- 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',
1859
2507
  };
1860
2508
  }
1861
- const postCommitInput = currentWorkValidationInput(validated.worktreeRepository, current, []);
1862
- current = transact('commit-no-checks-validation', (fresh) => writeWorkHandle(ctx.controllerHome, {
1863
- ...fresh,
1864
- expectedHead: postCommitInput.head,
1865
- validatedInputFingerprint: postCommitInput.fingerprint,
1866
- }));
1867
- 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
+ }
1868
2550
  } else if (!wants.commit && current.finalization.commit === 'pending') {
1869
2551
  current = transact('commit-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, commit: 'skipped' } }));
1870
2552
  }
1871
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
+
1872
2579
  if (wants.merge && current.finalization.merge === 'pending') {
1873
- 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
+ }
1874
2588
  const contract = contractFor(ctx, current);
1875
2589
  if (contract?.constraints.allowMerge === false) throw new Error('WORK_MERGE_NOT_ALLOWED: WorkContract disallows merge');
1876
2590
  const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
1877
- const deleteAfterWorktreeCleanup = current.managedWorktree && args.delete_branch !== false;
1878
- 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
+ }
1879
2843
  const pendingAuthorization = merged.steps.find((step) => step.execution.authorizationDecision?.decision === 'user_confirmation_required')?.execution.authorizationDecision;
1880
2844
  if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
1881
2845
  if (!merged.completed) return { ...failStage('merge', merged.error?.message ?? 'merge failed'), merge: merged };
1882
2846
  current = transact('merge-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
1883
- 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 },
1884
2848
  failureReason: undefined,
1885
2849
  }));
1886
2850
  } else if (!wants.merge && current.finalization.merge === 'pending') {
1887
- 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
+ }
1888
2916
  }
1889
2917
 
1890
2918
  if (wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
@@ -1906,7 +2934,11 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1906
2934
  current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
1907
2935
  }
1908
2936
 
1909
- 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
+ ) {
1910
2942
  const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
1911
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 });
1912
2944
  if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
@@ -1944,18 +2976,64 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1944
2976
  });
1945
2977
  }
1946
2978
  const receipt = completionReceiptForFinalizedWork(ctx, current, completionContract, args);
1947
- 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(
1948
2986
  { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1949
- workId,
1950
- receipt,
1951
- requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'completed_changed',
1952
- 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
+ ),
1953
3005
  );
3006
+ if (!fencedCompletion.allowed) {
3007
+ throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fencedCompletion.reason}`);
3008
+ }
3009
+ const recorded = fencedCompletion.value;
1954
3010
  if (recorded.requirementId) {
1955
- completeRequirementFromWork(
1956
- { controllerHome: ctx.controllerHome },
1957
- { requirementId: recorded.requirementId, work: recorded },
1958
- );
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
+ }
1959
3037
  }
1960
3038
  }
1961
3039
  // Successful WorkContract completion always ends controller ownership.
@@ -1967,7 +3045,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
1967
3045
  activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
1968
3046
  });
1969
3047
  }
1970
- 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' };
1971
3049
  }
1972
3050
 
1973
3051
  export async function callExecutionTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {