@moretea-labs/forge 1.7.0 → 1.7.2

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 (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +125 -101
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +69 -29
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +150 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +815 -1015
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +3 -2
  166. package/src/runtime/root/package-runtime-service.ts +48 -3
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -0,0 +1,1913 @@
1
+ import { listControllerChecks } from '../../../cli/controller/check-runner';
2
+ import type { ControllerCheck } from '../../../cli/controller/check-runner';
3
+ import type { CompletionReceipt } from '../../../cli/controller/types';
4
+ import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
5
+ import { globMatches } from '../../../cli/mcp/paths';
6
+ import { withControllerLock } from '../../../cli/repositories/locks';
7
+ import { getRepository, selectRepositoryCheckout, setRepositoryCheckoutLifecycle } from '../../../cli/repositories/registry';
8
+ import { repositoryGitCommit, repositoryGitDeleteBranch, repositoryGitFinishWorkflow, repositoryGitRebaseOnto, repositoryGitStatus } from '../../../cli/repositories/structured-git';
9
+ import type { RepositoryRecord } from '../../../cli/repositories/types';
10
+ import { hasCurrentWorkValidationAuthority, markWorkValidationPending, projectWorkValidationOutcome } from './work-validation-reconciler';
11
+ import { markRepositoryProjectionDirty } from '../../projections/invalidation';
12
+ import { withControllerSessionTerminalizationFence } from '../facade/controller-session-store';
13
+ import type { VerificationRecord } from '../facade/types';
14
+ import { appendVerificationRecord, appendWorkEvidence, listWorkContracts, transitionWorkContractPhase, updateWorkContract } from '../facade/work-contract-store';
15
+ import { readRepositoryAccessPolicy } from '../governance/access-policy';
16
+ import { assertResolvedAuthorization, decideAuthorization } from '../governance/authorization';
17
+ import { updateExecutionSession } from './session-store';
18
+ import { validateWorkHandle } from './validation';
19
+ import { effectiveVerificationEvidence, verificationInputFingerprint, workspaceValidationFingerprint, workValidationInputFingerprint } from './verification-evidence';
20
+ import { completeWorkWithReceipt } from './work-completion-authority';
21
+ import { markWorkHandleFailed, readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, writeWorkHandle } from './work-handle-store';
22
+ import type { WorkFinalizationStages, WorkHandleState } from './work-handle-store';
23
+ import { findWorkPathScopeViolation } from './work-path-scope';
24
+ import type { WorkRemoteDeliveryReceipt } from './work-remote-delivery';
25
+ import { pushExactWorkRemoteDelivery } from './work-remote-delivery';
26
+ import { spawnSync } from 'child_process';
27
+ import { createHash } from 'crypto';
28
+ import { existsSync, realpathSync } from 'fs';
29
+ import { basename, resolve } from 'path';
30
+ import { assertWorkControllerOwnership, compactHandle, contractFor, gitChangedPaths, gitCommit, gitHead, gitMergeBase, gitRevision, identityFor, reconcileTerminalCleanup, releasePreparedWorkOwnership, requireSession, selectWorkFinalizationTarget, terminalCleanupOutcome, workForSession, workReturnCheckoutId } from './work-execution-support';
31
+
32
+ function gitIsAncestor(root: string, ancestor: string, descendant: string): boolean {
33
+ const result = spawnSync('git', ['-C', root, 'merge-base', '--is-ancestor', ancestor, descendant], {
34
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
35
+ });
36
+ if (result.error || (result.status !== 0 && result.status !== 1)) {
37
+ throw new Error(`WORK_TARGET_ADVANCE_ANCESTRY_UNAVAILABLE: ${ancestor} -> ${descendant}`);
38
+ }
39
+ return result.status === 0;
40
+ }
41
+
42
+ export interface WorkTargetAdvanceInspection {
43
+ relation: 'candidate_contains_target' | 'target_contains_candidate' | 'diverged_clean' | 'diverged_conflict';
44
+ candidateHead: string;
45
+ targetHead: string;
46
+ mergeBase: string;
47
+ candidateChangedPaths: string[];
48
+ targetChangedPaths: string[];
49
+ mergedTree?: string;
50
+ detail?: string;
51
+ }
52
+
53
+ export function inspectWorkTargetAdvance(root: string, candidateRevision: string, targetRevision: string): WorkTargetAdvanceInspection {
54
+ const candidateHead = gitCommit(root, candidateRevision, 'TARGET_ADVANCE_CANDIDATE');
55
+ const targetHead = gitCommit(root, targetRevision, 'TARGET_ADVANCE_TARGET');
56
+ if (gitIsAncestor(root, targetHead, candidateHead)) {
57
+ return {
58
+ relation: 'candidate_contains_target', candidateHead, targetHead, mergeBase: targetHead,
59
+ candidateChangedPaths: gitChangedPaths(root, targetHead, candidateHead), targetChangedPaths: [],
60
+ };
61
+ }
62
+ if (gitIsAncestor(root, candidateHead, targetHead)) {
63
+ return {
64
+ relation: 'target_contains_candidate', candidateHead, targetHead, mergeBase: candidateHead,
65
+ candidateChangedPaths: [], targetChangedPaths: gitChangedPaths(root, candidateHead, targetHead),
66
+ };
67
+ }
68
+ const mergeBase = gitMergeBase(root, candidateHead, targetHead);
69
+ const candidateChangedPaths = gitChangedPaths(root, mergeBase, candidateHead);
70
+ const targetChangedPaths = gitChangedPaths(root, mergeBase, targetHead);
71
+ const preflight = spawnSync('git', ['-C', root, 'merge-tree', '--write-tree', candidateHead, targetHead], {
72
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: 512 * 1024,
73
+ });
74
+ if (preflight.error || (preflight.status !== 0 && preflight.status !== 1)) {
75
+ throw new Error(`WORK_TARGET_ADVANCE_PREFLIGHT_UNAVAILABLE: ${preflight.error?.message ?? preflight.stderr ?? 'merge-tree failed'}`);
76
+ }
77
+ if (preflight.status === 0) {
78
+ const mergedTree = typeof preflight.stdout === 'string' ? preflight.stdout.trim().split(/\s+/)[0] : undefined;
79
+ return {
80
+ relation: 'diverged_clean', candidateHead, targetHead, mergeBase, candidateChangedPaths, targetChangedPaths,
81
+ ...(mergedTree ? { mergedTree } : {}),
82
+ };
83
+ }
84
+ const detail = `${preflight.stdout ?? ''}\n${preflight.stderr ?? ''}`.trim().slice(0, 1_000);
85
+ return { relation: 'diverged_conflict', candidateHead, targetHead, mergeBase, candidateChangedPaths, targetChangedPaths, ...(detail ? { detail } : {}) };
86
+ }
87
+
88
+ export function targetAdvanceLinearMergeCommits(root: string, targetRevision: string, candidateRevision: string): string[] {
89
+ const targetHead = gitCommit(root, targetRevision, 'TARGET_ADVANCE_LINEAR_TARGET');
90
+ const candidateHead = gitCommit(root, candidateRevision, 'TARGET_ADVANCE_LINEAR_CANDIDATE');
91
+ const output = spawnSync('git', ['-C', root, 'rev-list', '--merges', `${targetHead}..${candidateHead}`], {
92
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: 512 * 1024,
93
+ });
94
+ if (output.status !== 0 || output.error || typeof output.stdout !== 'string') {
95
+ throw new Error('WORK_TARGET_ADVANCE_LINEAR_HISTORY_UNAVAILABLE: could not inspect candidate merge history');
96
+ }
97
+ return output.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
98
+ }
99
+
100
+ function normalizedReadScope(scope: string): string {
101
+ return scope.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '') || '.';
102
+ }
103
+
104
+ function pathIsWithinReadScope(path: string, scope: string): boolean {
105
+ const normalized = normalizedReadScope(scope);
106
+ return normalized === '.' || path === normalized || path.startsWith(`${normalized}/`);
107
+ }
108
+
109
+ function stableCheckDefinition(check: ControllerCheck | undefined): string | undefined {
110
+ if (!check) return undefined;
111
+ return JSON.stringify({
112
+ id: check.id,
113
+ command: check.command,
114
+ cwd: check.cwd,
115
+ timeoutMs: check.timeoutMs,
116
+ source: check.source,
117
+ effects: check.effects ?? null,
118
+ });
119
+ }
120
+
121
+ export interface TargetAdvanceValidationTransferPlan {
122
+ transferredRecords: VerificationRecord[];
123
+ reusableCheckIds: string[];
124
+ invalidatedCheckIds: string[];
125
+ }
126
+
127
+ export function planTargetAdvanceValidationAuthority(input: {
128
+ checkIds: string[];
129
+ checkRefs: VerificationRecord[];
130
+ checksBefore: ControllerCheck[];
131
+ checksAfter: ControllerCheck[];
132
+ candidateHead: string;
133
+ candidateWorkspaceFingerprint: string;
134
+ integratedHead: string;
135
+ integratedWorkspaceFingerprint: string;
136
+ targetChangedPaths: string[];
137
+ recordedAt?: string;
138
+ }): TargetAdvanceValidationTransferPlan {
139
+ const beforeById = new Map(input.checksBefore.map((check) => [check.id, check]));
140
+ const afterById = new Map(input.checksAfter.map((check) => [check.id, check]));
141
+ const transferredRecords: VerificationRecord[] = [];
142
+ const reusableCheckIds: string[] = [];
143
+ const invalidatedCheckIds: string[] = [];
144
+ const recordedAt = input.recordedAt ?? new Date().toISOString();
145
+ for (const checkId of input.checkIds) {
146
+ const before = beforeById.get(checkId);
147
+ const after = afterById.get(checkId);
148
+ const reads = before?.effects?.reads;
149
+ const definitionUnchanged = stableCheckDefinition(before) !== undefined
150
+ && stableCheckDefinition(before) === stableCheckDefinition(after);
151
+ const readsGitHistory = before?.effects?.git !== undefined;
152
+ const targetTouchesReadScope = !reads || reads.length === 0
153
+ || input.targetChangedPaths.some((path) => reads.some((scope) => pathIsWithinReadScope(path, scope)));
154
+ const sourcePass = effectiveVerificationEvidence(input.checkRefs, {
155
+ sourceRevision: input.candidateHead,
156
+ workspaceFingerprint: input.candidateWorkspaceFingerprint,
157
+ checkId,
158
+ requestedChecks: input.checkIds,
159
+ }).find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))?.record;
160
+ if (!definitionUnchanged || readsGitHistory || targetTouchesReadScope || !sourcePass) {
161
+ invalidatedCheckIds.push(checkId);
162
+ continue;
163
+ }
164
+ const transferred: VerificationRecord = {
165
+ ...sourcePass,
166
+ summary: `Validation authority transferred across target advancement because target-only changes did not intersect check ${checkId} read inputs.`,
167
+ recordedAt,
168
+ sourceRevision: input.integratedHead,
169
+ workspaceFingerprint: input.integratedWorkspaceFingerprint,
170
+ verificationInputFingerprint: verificationInputFingerprint({
171
+ sourceRevision: input.integratedHead,
172
+ workspaceFingerprint: input.integratedWorkspaceFingerprint,
173
+ checkId,
174
+ requestedChecks: input.checkIds,
175
+ }),
176
+ evidenceRef: {
177
+ title: checkId,
178
+ summary: `Reused prior valid receipt after proving target-only path changes do not affect this check's declared inputs.`,
179
+ detailLevel: 'summary',
180
+ },
181
+ };
182
+ transferredRecords.push(transferred);
183
+ reusableCheckIds.push(checkId);
184
+ }
185
+ return { transferredRecords, reusableCheckIds, invalidatedCheckIds };
186
+ }
187
+
188
+ export function targetAdvanceWorkScopeViolation(
189
+ scope: { allowedPaths: string[]; forbiddenPaths: string[] } | undefined,
190
+ changedPaths: string[],
191
+ ): { kind: 'forbidden' | 'out_of_scope'; path: string } | undefined {
192
+ if (!scope) return undefined;
193
+ const violation = findWorkPathScopeViolation(scope, changedPaths);
194
+ return violation ? { kind: violation.kind, path: violation.path } : undefined;
195
+ }
196
+
197
+ function replaceTargetAdvanceScopeEvidence(
198
+ ctx: MultiRepositoryMcpToolContext,
199
+ contract: NonNullable<ReturnType<typeof contractFor>>,
200
+ actualChangedPaths: string[],
201
+ ): void {
202
+ updateWorkContract({ controllerHome: ctx.controllerHome, repoId: contract.repoId }, contract.workId, {
203
+ scopeEvidence: {
204
+ initialLikelyPaths: contract.scopeEvidence?.initialLikelyPaths ?? contract.allowedPaths,
205
+ inspectedPaths: contract.scopeEvidence?.inspectedPaths ?? [],
206
+ actualChangedPaths: [...new Set(actualChangedPaths)].sort((left, right) => left.localeCompare(right)).slice(0, 500),
207
+ recordedAt: new Date().toISOString(),
208
+ },
209
+ });
210
+ }
211
+
212
+ export interface DirectTargetDeliveryInspection {
213
+ integrated: boolean;
214
+ reason: 'not_direct_target' | 'path_mismatch' | 'branch_mismatch' | 'dirty' | 'revision_unavailable' | 'not_reachable' | 'integrated';
215
+ expectedHead?: string;
216
+ targetHead?: string;
217
+ }
218
+
219
+ /**
220
+ * Direct-edit Work may already be executing on the delivery branch. In that
221
+ * case Git merge is not a delivery operation at all: the Work commit is
222
+ * integrated once its exact expected revision remains reachable from the
223
+ * target branch. Keep this proof deliberately strict so a feature Work or a
224
+ * dirty/mismatched checkout still follows the normal fail-closed merge path.
225
+ */
226
+ export function inspectDirectTargetDelivery(
227
+ root: string,
228
+ worktreePath: string,
229
+ managedWorktree: boolean,
230
+ workBranch: string,
231
+ targetBranch: string,
232
+ expectedRevision: string | undefined,
233
+ ): DirectTargetDeliveryInspection {
234
+ if (managedWorktree || workBranch !== targetBranch) return { integrated: false, reason: 'not_direct_target' };
235
+ try {
236
+ if (realpathSync(root) !== realpathSync(worktreePath)) return { integrated: false, reason: 'path_mismatch' };
237
+ } catch {
238
+ return { integrated: false, reason: 'path_mismatch' };
239
+ }
240
+ const branch = spawnSync('git', ['-C', root, 'branch', '--show-current'], {
241
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
242
+ });
243
+ if (branch.status !== 0 || typeof branch.stdout !== 'string' || branch.stdout.trim() !== targetBranch) {
244
+ return { integrated: false, reason: 'branch_mismatch' };
245
+ }
246
+ const status = spawnSync('git', ['-C', root, 'status', '--porcelain'], {
247
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
248
+ });
249
+ if (status.status !== 0 || typeof status.stdout !== 'string' || status.stdout.trim()) {
250
+ return { integrated: false, reason: 'dirty' };
251
+ }
252
+ const expectedHead = expectedRevision ? gitRevision(root, expectedRevision) : undefined;
253
+ const targetHead = gitRevision(root, targetBranch);
254
+ if (!expectedHead || !targetHead) return { integrated: false, reason: 'revision_unavailable' };
255
+ if (!gitIsAncestor(root, expectedHead, targetHead)) {
256
+ return { integrated: false, reason: 'not_reachable', expectedHead, targetHead };
257
+ }
258
+ return { integrated: true, reason: 'integrated', expectedHead, targetHead };
259
+ }
260
+
261
+ function exactCommitChangedPaths(repoRoot: string, revision: string): string[] {
262
+ const result = spawnSync('git', ['-C', repoRoot, 'diff-tree', '--root', '--no-commit-id', '--name-only', '-r', revision], {
263
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
264
+ });
265
+ if (result.status !== 0 || result.error || typeof result.stdout !== 'string') {
266
+ throw new Error(`WORK_COMPLETION_RECEIPT_COMMIT_PATHS_UNAVAILABLE: ${revision}`);
267
+ }
268
+ return Array.from(new Set(result.stdout.split('\n').map((entry) => entry.trim()).filter(Boolean)))
269
+ .sort((left, right) => left.localeCompare(right));
270
+ }
271
+
272
+ function workOwnedDirtyPaths(
273
+ contract: { allowedPaths: string[]; forbiddenPaths: string[] } | undefined,
274
+ status: ReturnType<typeof repositoryGitStatus>,
275
+ ): { dirtyPaths: string[]; ownedPaths: string[] } {
276
+ const dirtyPaths = [...new Set([...status.staged, ...status.unstaged, ...status.untracked])]
277
+ .sort((left, right) => left.localeCompare(right));
278
+ if (!contract) return { dirtyPaths, ownedPaths: dirtyPaths };
279
+ const ownedPaths = dirtyPaths.filter((path) => {
280
+ if (contract.forbiddenPaths.some((pattern) => globMatches(pattern, path))) return false;
281
+ return contract.allowedPaths.length === 0 || contract.allowedPaths.some((pattern) => globMatches(pattern, path));
282
+ });
283
+ return { dirtyPaths, ownedPaths };
284
+ }
285
+
286
+ export interface TargetDirtyWorkOwnershipInspection {
287
+ owned: boolean;
288
+ dirtyPaths: string[];
289
+ owners: Record<string, string>;
290
+ unownedPaths: string[];
291
+ ambiguousPaths: string[];
292
+ }
293
+
294
+ /**
295
+ * Positive ownership proof for dirty content preserved in a shared canonical
296
+ * target checkout. Exact observed Work scope wins; a unique explicit allow-list
297
+ * is the fallback. Repository-wide/legacy scope is never enough to move another
298
+ * Work's dirty content through target integration.
299
+ */
300
+ export function inspectTargetDirtyWorkOwnership(input: {
301
+ dirtyPaths: string[];
302
+ targetCheckoutId: string;
303
+ currentWorkId: string;
304
+ activeWorks: Array<{
305
+ workId: string;
306
+ checkoutId?: string;
307
+ allowedPaths: string[];
308
+ forbiddenPaths: string[];
309
+ scopeEvidence?: { actualChangedPaths: string[] };
310
+ }>;
311
+ }): TargetDirtyWorkOwnershipInspection {
312
+ const dirtyPaths = [...new Set(input.dirtyPaths.map((path) => path.trim()).filter(Boolean))].sort();
313
+ const candidates = input.activeWorks.filter((work) =>
314
+ work.workId !== input.currentWorkId
315
+ && work.checkoutId === input.targetCheckoutId,
316
+ );
317
+ const owners: Record<string, string> = {};
318
+ const unownedPaths: string[] = [];
319
+ const ambiguousPaths: string[] = [];
320
+ for (const path of dirtyPaths) {
321
+ const exactOwners = candidates.filter((work) => work.scopeEvidence?.actualChangedPaths?.includes(path));
322
+ const scopedOwners = exactOwners.length > 0 ? exactOwners : candidates.filter((work) =>
323
+ work.allowedPaths.length > 0
324
+ && !work.forbiddenPaths.some((pattern) => globMatches(pattern, path))
325
+ && work.allowedPaths.some((pattern) => globMatches(pattern, path)),
326
+ );
327
+ const ownerIds = [...new Set(scopedOwners.map((work) => work.workId))].sort();
328
+ if (ownerIds.length === 1) owners[path] = ownerIds[0];
329
+ else if (ownerIds.length === 0) unownedPaths.push(path);
330
+ else ambiguousPaths.push(path);
331
+ }
332
+ return {
333
+ owned: dirtyPaths.length > 0 && unownedPaths.length === 0 && ambiguousPaths.length === 0,
334
+ dirtyPaths,
335
+ owners,
336
+ unownedPaths,
337
+ ambiguousPaths,
338
+ };
339
+ }
340
+
341
+ export interface DirectCanonicalTargetAdvanceInspection {
342
+ reconcilable: boolean;
343
+ reason:
344
+ | 'not_direct_target'
345
+ | 'path_mismatch'
346
+ | 'branch_mismatch'
347
+ | 'revision_unavailable'
348
+ | 'target_not_current'
349
+ | 'not_descendant'
350
+ | 'explicit_scope_required'
351
+ | 'owned_dirty_required'
352
+ | 'unrelated_dirty_paths'
353
+ | 'target_touches_work_path'
354
+ | 'fresh_verification_required'
355
+ | 'fresh_verification_missing'
356
+ | 'reconcilable';
357
+ previousHead?: string;
358
+ targetHead?: string;
359
+ dirtyPaths: string[];
360
+ targetChangedPaths: string[];
361
+ workspaceFingerprint?: string;
362
+ freshCheckIds: string[];
363
+ }
364
+
365
+ /**
366
+ * Direct canonical Work is intentionally stricter than managed-worktree delivery:
367
+ * the shared checkout may contain preserved dirty content owned by another Work.
368
+ * Reconcile a stale expectedHead only when the target advanced linearly, every
369
+ * current dirty path is positively owned by this Work, target-only commits do not
370
+ * touch those dirty paths, and exact current source/workspace verification exists.
371
+ * The caller may then move only delivery identity; this function never attributes
372
+ * target-only commits or unrelated dirty paths to the Work.
373
+ */
374
+ export function inspectDirectCanonicalTargetAdvanceReconciliation(input: {
375
+ root: string;
376
+ worktreePath: string;
377
+ managedWorktree: boolean;
378
+ workBranch: string;
379
+ targetBranch: string;
380
+ expectedRevision?: string;
381
+ status: ReturnType<typeof repositoryGitStatus>;
382
+ scope?: { allowedPaths: string[]; forbiddenPaths: string[] };
383
+ checkIds: string[];
384
+ checkRefs: VerificationRecord[];
385
+ }): DirectCanonicalTargetAdvanceInspection {
386
+ const empty = (reason: DirectCanonicalTargetAdvanceInspection['reason']): DirectCanonicalTargetAdvanceInspection => ({
387
+ reconcilable: false,
388
+ reason,
389
+ dirtyPaths: [],
390
+ targetChangedPaths: [],
391
+ freshCheckIds: [],
392
+ });
393
+ if (input.managedWorktree || input.workBranch !== input.targetBranch) return empty('not_direct_target');
394
+ try {
395
+ if (realpathSync(input.root) !== realpathSync(input.worktreePath)) return empty('path_mismatch');
396
+ } catch {
397
+ return empty('path_mismatch');
398
+ }
399
+ const branch = spawnSync('git', ['-C', input.root, 'branch', '--show-current'], {
400
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
401
+ });
402
+ if (branch.status !== 0 || typeof branch.stdout !== 'string' || branch.stdout.trim() !== input.targetBranch) {
403
+ return empty('branch_mismatch');
404
+ }
405
+ const previousHead = input.expectedRevision ? gitRevision(input.root, input.expectedRevision) : undefined;
406
+ const targetHead = gitRevision(input.root, input.targetBranch);
407
+ const currentHead = input.status.head ? gitRevision(input.root, input.status.head) : undefined;
408
+ if (!previousHead || !targetHead || !currentHead) return empty('revision_unavailable');
409
+ const base = {
410
+ previousHead,
411
+ targetHead,
412
+ dirtyPaths: [] as string[],
413
+ targetChangedPaths: [] as string[],
414
+ freshCheckIds: [] as string[],
415
+ };
416
+ if (targetHead !== currentHead) return { ...base, reconcilable: false, reason: 'target_not_current' };
417
+ if (targetHead === previousHead || !gitIsAncestor(input.root, previousHead, targetHead)) {
418
+ return { ...base, reconcilable: false, reason: 'not_descendant' };
419
+ }
420
+ // Positive allow-list ownership is required here. An empty allow-list means
421
+ // repository-wide scope and cannot distinguish another Work's preserved dirty
422
+ // content in a shared canonical checkout.
423
+ if (!input.scope || input.scope.allowedPaths.length === 0) {
424
+ return { ...base, reconcilable: false, reason: 'explicit_scope_required' };
425
+ }
426
+ const { dirtyPaths, ownedPaths } = workOwnedDirtyPaths(input.scope, input.status);
427
+ const dirtyBase = { ...base, dirtyPaths };
428
+ if (dirtyPaths.length === 0 || ownedPaths.length === 0) {
429
+ return { ...dirtyBase, reconcilable: false, reason: 'owned_dirty_required' };
430
+ }
431
+ if (ownedPaths.length !== dirtyPaths.length) {
432
+ return { ...dirtyBase, reconcilable: false, reason: 'unrelated_dirty_paths' };
433
+ }
434
+ const targetChangedPaths = gitChangedPaths(input.root, previousHead, targetHead);
435
+ const targetBase = { ...dirtyBase, targetChangedPaths };
436
+ const dirtySet = new Set(dirtyPaths);
437
+ if (targetChangedPaths.some((path) => dirtySet.has(path))) {
438
+ return { ...targetBase, reconcilable: false, reason: 'target_touches_work_path' };
439
+ }
440
+ const workspaceFingerprint = workspaceValidationFingerprint(input.root, input.status);
441
+ const declaredCheckIds = [...new Set(input.checkIds)].sort((left, right) => left.localeCompare(right));
442
+ const candidateCheckIds = declaredCheckIds.length > 0
443
+ ? declaredCheckIds
444
+ : [...new Set(input.checkRefs.map((record) => record.checkId).filter(Boolean))].sort((left, right) => left.localeCompare(right));
445
+ if (candidateCheckIds.length === 0) {
446
+ return { ...targetBase, workspaceFingerprint, reconcilable: false, reason: 'fresh_verification_required' };
447
+ }
448
+ const freshCheckIds = candidateCheckIds.filter((checkId) => effectiveVerificationEvidence(input.checkRefs, {
449
+ sourceRevision: targetHead,
450
+ workspaceFingerprint,
451
+ checkId,
452
+ // Contract-declared checks share one validation-input identity. When the
453
+ // contract has no declared checks, rh_work verify can still append an
454
+ // explicit focused check receipt; such evidence is bound to its one-check
455
+ // request and must be evaluated with that exact requested-check set.
456
+ requestedChecks: declaredCheckIds.length > 0 ? declaredCheckIds : [checkId],
457
+ }).some((entry) => entry.current
458
+ && entry.record.outcome === 'valid_pass'
459
+ && entry.record.receipt?.ok === true
460
+ && entry.record.receipt.status === 'passed'
461
+ && entry.record.receipt.runtimeStatus === 'succeeded'));
462
+ const freshVerificationSatisfied = declaredCheckIds.length > 0
463
+ ? freshCheckIds.length === declaredCheckIds.length
464
+ : freshCheckIds.length > 0;
465
+ if (!freshVerificationSatisfied) {
466
+ return { ...targetBase, workspaceFingerprint, freshCheckIds, reconcilable: false, reason: 'fresh_verification_missing' };
467
+ }
468
+ return {
469
+ ...targetBase,
470
+ workspaceFingerprint,
471
+ freshCheckIds,
472
+ reconcilable: true,
473
+ reason: 'reconcilable',
474
+ };
475
+ }
476
+
477
+ export function completionReceiptChangedPaths(repoRoot: string, baseRevision: string, deliveryRevision: string): string[] {
478
+ const result = spawnSync('git', ['-C', repoRoot, 'diff', '--name-only', `${baseRevision}..${deliveryRevision}`], {
479
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
480
+ });
481
+ if (result.status !== 0 || result.error || typeof result.stdout !== 'string') {
482
+ throw new Error(`WORK_COMPLETION_RECEIPT_CHANGED_PATHS_UNAVAILABLE: ${baseRevision}..${deliveryRevision}`);
483
+ }
484
+ return Array.from(new Set(result.stdout.split('\n').map((entry) => entry.trim()).filter(Boolean)))
485
+ .sort((left, right) => left.localeCompare(right));
486
+ }
487
+
488
+ function completionReceiptForFinalizedWork(
489
+ ctx: MultiRepositoryMcpToolContext,
490
+ handle: WorkHandleState,
491
+ contract: ReturnType<typeof contractFor>,
492
+ args: Record<string, unknown>,
493
+ ): CompletionReceipt {
494
+ const repository = getRepository(handle.repositoryId, ctx.controllerHome, { includeRemoved: true });
495
+ const target = selectWorkFinalizationTarget(repository, handle);
496
+ const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
497
+ ? args.target_branch.trim()
498
+ : repository.defaultBranch || 'main';
499
+ const targetRevision = gitRevision(target.canonicalRoot, targetBranch);
500
+ if (!targetRevision) throw new Error(`WORK_COMPLETION_RECEIPT_TARGET_REQUIRED: target branch ${targetBranch} is unavailable`);
501
+ const noChange = args.completion_outcome === 'completed_no_change';
502
+ // Delivery authority is always the Work's own revision, never the target
503
+ // branch's newest HEAD. The target may have advanced because of unrelated
504
+ // concurrent Work; targetRevision records integration identity, while
505
+ // deliveryRevision owns path attribution and reachability proof.
506
+ const deliveryRevision = handle.expectedHead ?? handle.baseCommit;
507
+ if (!deliveryRevision) throw new Error('WORK_COMPLETION_RECEIPT_SOURCE_REQUIRED: Work delivery revision is unavailable');
508
+ const reachable = deliveryRevision === targetRevision || spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', deliveryRevision, targetBranch], {
509
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
510
+ }).status === 0;
511
+ if (!reachable) throw new Error(`WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN: ${deliveryRevision} is not reachable from ${targetBranch}`);
512
+ const deliveryBaseRevision = workDeliveryBaseRevision(handle);
513
+ const changedPaths = noChange
514
+ ? []
515
+ : !handle.managedWorktree && handle.finalization.commit === 'done'
516
+ ? exactCommitChangedPaths(target.canonicalRoot, deliveryRevision)
517
+ : deliveryBaseRevision
518
+ ? completionReceiptChangedPaths(target.canonicalRoot, deliveryBaseRevision, deliveryRevision)
519
+ : (() => { throw new Error('WORK_COMPLETION_RECEIPT_BASE_REQUIRED: Work delivery provenance base is unavailable'); })();
520
+ const recordedAt = new Date().toISOString();
521
+ const retainsExplicitDeliveryBranch = args.merge !== true && targetBranch === handle.branch;
522
+ const warnings = [
523
+ ...(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 }] : []),
524
+ ...(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 }] : []),
525
+ ];
526
+ return {
527
+ schemaVersion: 1,
528
+ receiptId: `REC-controller_work-${createHash('sha256').update(`${handle.workId}\0${targetRevision}`).digest('hex').slice(0, 16)}`,
529
+ source: 'controller_work',
530
+ issueId: contract?.issueId ?? 'work',
531
+ taskId: contract?.taskId ?? handle.workId,
532
+ workId: handle.workId,
533
+ targetBranch,
534
+ targetRevision,
535
+ sourceRevision: deliveryRevision,
536
+ baseRevision: handle.baseCommit,
537
+ changedPaths,
538
+ delivery: {
539
+ kind: noChange ? 'no_change' : 'commit',
540
+ status: 'integrated',
541
+ strategy: noChange ? 'no_change' : handle.finalization.commit === 'done' ? 'edit_session_commit' : 'already_integrated',
542
+ reachable: true,
543
+ recordedAt,
544
+ },
545
+ cleanup: {
546
+ status: warnings.length > 0 ? 'maintenance_warning' : 'complete',
547
+ warnings,
548
+ blockers: [],
549
+ recordedAt,
550
+ },
551
+ verifiedAt: recordedAt,
552
+ recordedAt,
553
+ };
554
+ }
555
+
556
+ function runCleanup(targetRoot: string, worktreePath: string): { ok: boolean; message?: string } {
557
+ if (targetRoot === worktreePath) return { ok: true };
558
+ if (!existsSync(worktreePath)) return { ok: true, message: 'managed worktree already removed' };
559
+ const status = repositoryGitStatus({ repoId: 'cleanup', activeCheckoutId: 'cleanup', canonicalRoot: worktreePath, localRoot: worktreePath, checkouts: [], schemaVersion: 1, displayName: basename(worktreePath), repositoryType: 'git', enabled: true, createdAt: '', updatedAt: '', lastSeenAt: '', configurationPath: '', stateStorageStrategy: 'controller-home' });
560
+ if (!status.clean) return { ok: false, message: 'managed worktree is dirty; cleanup preserved it' };
561
+ const process = spawnSync('git', ['-C', targetRoot, 'worktree', 'remove', worktreePath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000 });
562
+ return process.status === 0 ? { ok: true } : { ok: false, message: String(process.stderr ?? 'git worktree remove failed').trim() };
563
+ }
564
+
565
+ /**
566
+ * Reconcile a stale Work Handle only for a cleanup-only request whose exact
567
+ * branch HEAD is already reachable from the requested target branch.
568
+ *
569
+ * Two bounded recovery cases are supported:
570
+ * 1. an older controller committed/merged successfully but failed before
571
+ * recording the finalization stages; and
572
+ * 2. a terminal cancelled WorkContract owns an unchanged, clean duplicate
573
+ * worktree created by an idempotency defect.
574
+ *
575
+ * Missing worktrees are accepted only when this handle already recorded a
576
+ * successful worktree cleanup. This lets a retry finish branch cleanup after a
577
+ * crash without turning a missing checkout into proof of safety.
578
+ */
579
+ export function inspectCleanupOnlyMergedHead(
580
+ ctx: MultiRepositoryMcpToolContext,
581
+ current: WorkHandleState,
582
+ args: Record<string, unknown>,
583
+ ): { currentHead: string; cancelledContract: boolean; worktreeMissing: boolean } | undefined {
584
+ if (args.cleanup !== true || args.commit === true || args.merge === true || !current.managedWorktree) return undefined;
585
+ const contract = contractFor(ctx, current);
586
+ const cancelledContract = contract?.status === 'cancelled';
587
+
588
+ const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
589
+ const target = selectWorkFinalizationTarget(repository, current);
590
+ const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
591
+ ? args.target_branch.trim()
592
+ : repository.defaultBranch || 'main';
593
+ const branchHeadResult = spawnSync('git', ['-C', target.canonicalRoot, 'rev-parse', '--verify', `refs/heads/${current.branch}`], {
594
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
595
+ });
596
+ const branchHead = branchHeadResult.status === 0 ? String(branchHeadResult.stdout ?? '').trim() : '';
597
+ // A previous cleanup attempt may already have deleted the Work branch before
598
+ // completion-receipt persistence failed. Only the exact durable expectedHead
599
+ // may replace that missing ref, and only after branchCleanup itself is durably
600
+ // recorded done.
601
+ const currentHead = branchHead
602
+ || (current.finalization.branchCleanup === 'done' ? current.expectedHead ?? '' : '');
603
+ if (!currentHead) return undefined;
604
+ const merged = spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', currentHead, targetBranch], {
605
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
606
+ });
607
+ if (merged.status !== 0) return undefined;
608
+
609
+ const worktreeMissing = !existsSync(current.worktreePath);
610
+ if (worktreeMissing) {
611
+ const integratedDeliveryRecorded = current.state === 'merged'
612
+ || current.state === 'cleaned'
613
+ || current.finalization.merge === 'done';
614
+ if (current.finalization.worktreeCleanup !== 'done') return undefined;
615
+ if (!cancelledContract && !integratedDeliveryRecorded) return undefined;
616
+ return { currentHead, cancelledContract, worktreeMissing: true };
617
+ }
618
+
619
+ const worktree = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
620
+ if (!repositoryGitStatus(worktree).clean) return undefined;
621
+ const worktreeHead = gitHead(worktree.canonicalRoot);
622
+ if (!worktreeHead || worktreeHead !== currentHead) return undefined;
623
+ const currentBranch = spawnSync('git', ['-C', worktree.canonicalRoot, 'branch', '--show-current'], {
624
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
625
+ });
626
+ if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
627
+ if (current.expectedHead && currentHead !== current.expectedHead && cancelledContract) return undefined;
628
+ return { currentHead, cancelledContract, worktreeMissing: false };
629
+ }
630
+
631
+ interface FailedCleanupProof {
632
+ currentHead: string;
633
+ worktreeMissing: boolean;
634
+ targetBranch: string;
635
+ }
636
+
637
+ /**
638
+ * Prove that a failed Work owns an unchanged, clean managed worktree whose
639
+ * branch is already contained in the target branch. This authorizes resource
640
+ * cleanup only; it never proves successful verification or delivery.
641
+ */
642
+ function failedCleanupOnlyHead(
643
+ ctx: MultiRepositoryMcpToolContext,
644
+ current: WorkHandleState,
645
+ args: Record<string, unknown>,
646
+ ): FailedCleanupProof | undefined {
647
+ if (
648
+ current.state !== 'failed'
649
+ || args.cleanup !== true
650
+ || args.commit === true
651
+ || args.merge === true
652
+ || !current.managedWorktree
653
+ ) return undefined;
654
+ const contract = contractFor(ctx, current);
655
+ if (contract?.status !== 'failed') return undefined;
656
+
657
+ const expectedHead = current.expectedHead ?? current.baseCommit;
658
+ if (!expectedHead) return undefined;
659
+ const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
660
+ const target = selectWorkFinalizationTarget(repository, current);
661
+ const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
662
+ ? args.target_branch.trim()
663
+ : repository.defaultBranch || 'main';
664
+ const merged = spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', expectedHead, targetBranch], {
665
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
666
+ });
667
+ if (merged.status !== 0) return undefined;
668
+
669
+ const worktreeMissing = !existsSync(current.worktreePath);
670
+ if (worktreeMissing) {
671
+ return current.finalization.worktreeCleanup === 'done'
672
+ ? { currentHead: expectedHead, worktreeMissing: true, targetBranch }
673
+ : undefined;
674
+ }
675
+
676
+ const worktree = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
677
+ if (resolve(worktree.canonicalRoot) !== resolve(current.worktreePath)) return undefined;
678
+ if (!repositoryGitStatus(worktree).clean) return undefined;
679
+ if (gitHead(worktree.canonicalRoot) !== expectedHead) return undefined;
680
+ const currentBranch = spawnSync('git', ['-C', worktree.canonicalRoot, 'branch', '--show-current'], {
681
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
682
+ });
683
+ if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
684
+ return { currentHead: expectedHead, worktreeMissing: false, targetBranch };
685
+ }
686
+
687
+ type RetryableFinalizationStage = 'commit' | 'merge' | 'branchCleanup' | 'worktreeCleanup';
688
+
689
+ function requestedFailedFinalizationRetry(
690
+ stages: WorkFinalizationStages,
691
+ wants: { commit: boolean; merge: boolean; cleanup: boolean },
692
+ ): RetryableFinalizationStage | undefined {
693
+ if (stages.validation === 'failed') return undefined;
694
+ if (wants.commit && stages.commit === 'failed') return 'commit';
695
+ if (wants.merge && stages.merge === 'failed') return 'merge';
696
+ if (wants.cleanup && stages.worktreeCleanup === 'failed') return 'worktreeCleanup';
697
+ if (wants.cleanup && stages.branchCleanup === 'failed') return 'branchCleanup';
698
+ return undefined;
699
+ }
700
+
701
+ function retryPhaseForFinalizationStage(stage: RetryableFinalizationStage): 'delivery' | 'cleanup' {
702
+ return stage === 'commit' || stage === 'merge' ? 'delivery' : 'cleanup';
703
+ }
704
+
705
+ function retryHandleStateForFinalization(stages: WorkFinalizationStages): WorkHandleState['state'] {
706
+ if (stages.merge === 'done') return 'merged';
707
+ if (stages.commit === 'done') return 'committed';
708
+ return 'validating';
709
+ }
710
+
711
+ export function resetFinalizationStagesForRequest(
712
+ stages: WorkFinalizationStages,
713
+ wants: { commit: boolean; merge: boolean; cleanup: boolean },
714
+ options: { managedWorktree?: boolean; deleteBranchRequested?: boolean; retainedByRequest?: boolean; workspaceDirty?: boolean } = {},
715
+ ): WorkFinalizationStages {
716
+ const next = { ...stages };
717
+ let reset = false;
718
+ if (next.validation === 'failed') {
719
+ next.validation = 'pending';
720
+ reset = true;
721
+ }
722
+ if (wants.commit && next.commit === 'failed') {
723
+ next.commit = 'pending';
724
+ reset = true;
725
+ }
726
+ if (wants.merge && next.merge === 'failed') {
727
+ next.merge = 'pending';
728
+ reset = true;
729
+ }
730
+ if (wants.cleanup && next.worktreeCleanup === 'failed') {
731
+ next.worktreeCleanup = 'pending';
732
+ reset = true;
733
+ }
734
+ if (wants.cleanup && next.branchCleanup === 'failed') {
735
+ next.branchCleanup = 'pending';
736
+ reset = true;
737
+ }
738
+ // A Work may be semantically repaired and revalidated after an earlier delivery
739
+ // attempt already marked commit/merge done. Exact validation of a dirty current
740
+ // workspace proves there is new Work-owned content still requiring delivery;
741
+ // re-arm only the requested Git stages instead of letting cleanup discard it.
742
+ if (options.workspaceDirty === true && wants.commit && next.commit === 'done') {
743
+ next.commit = 'pending';
744
+ reset = true;
745
+ if (wants.merge && next.merge === 'done') next.merge = 'pending';
746
+ }
747
+ // A prior finalize(cleanup=false) intentionally records managed resources as
748
+ // skipped/retained. A later explicit cleanup=true is a new resource-disposal
749
+ // request, not an idempotent replay of the earlier retention decision. Re-arm
750
+ // only cleanup stages that are applicable to this managed Work; commit/merge
751
+ // skipped states keep their original delivery semantics.
752
+ if (wants.cleanup && options.retainedByRequest === true && options.managedWorktree === true && next.worktreeCleanup === 'skipped') {
753
+ next.worktreeCleanup = 'pending';
754
+ reset = true;
755
+ }
756
+ if (wants.cleanup && options.retainedByRequest === true && options.managedWorktree === true && options.deleteBranchRequested === true && next.branchCleanup === 'skipped') {
757
+ next.branchCleanup = 'pending';
758
+ reset = true;
759
+ }
760
+ if (reset) delete next.lastError;
761
+ return next;
762
+ }
763
+
764
+ function finalizationComplete(stages: WorkFinalizationStages): boolean {
765
+ return stages.validation === 'done'
766
+ && stages.commit !== 'pending'
767
+ && stages.merge !== 'pending'
768
+ && stages.branchCleanup !== 'pending'
769
+ && stages.worktreeCleanup !== 'pending'
770
+ && stages.commit !== 'failed'
771
+ && stages.merge !== 'failed'
772
+ && stages.branchCleanup !== 'failed'
773
+ && stages.worktreeCleanup !== 'failed'
774
+ && !stages.lastError;
775
+ }
776
+
777
+ function finalStateForStages(stages: WorkFinalizationStages, fallback: WorkHandleState['state']): WorkHandleState['state'] {
778
+ if (stages.worktreeCleanup === 'done') return 'cleaned';
779
+ if (stages.merge === 'done') return 'merged';
780
+ if (stages.commit === 'done') return 'committed';
781
+ return fallback === 'failed' ? 'editing' : fallback;
782
+ }
783
+
784
+ function currentWorkValidationInput(
785
+ repository: RepositoryRecord,
786
+ handle: WorkHandleState,
787
+ requestedChecks: string[],
788
+ ): { head: string; workspaceFingerprint: string; fingerprint: string; clean: boolean } {
789
+ const status = repositoryGitStatus(repository);
790
+ const head = status.head ?? handle.expectedHead ?? handle.baseCommit ?? 'unknown';
791
+ const workspaceFingerprint = workspaceValidationFingerprint(repository.canonicalRoot, status);
792
+ return {
793
+ head,
794
+ workspaceFingerprint,
795
+ fingerprint: workValidationInputFingerprint(head, workspaceFingerprint, requestedChecks),
796
+ clean: status.clean,
797
+ };
798
+ }
799
+
800
+ export async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
801
+ const session = requireSession(ctx, args);
802
+ let current = workForSession(ctx, session, args, { allowClaimedTerminalCleanup: args.cleanup !== false });
803
+ const requestedWants = { commit: args.commit === true, merge: args.merge === true, cleanup: args.cleanup === true };
804
+ const retryStage = current.state === 'failed'
805
+ ? requestedFailedFinalizationRetry(current.finalization, requestedWants)
806
+ : undefined;
807
+ const retryContract = retryStage ? contractFor(ctx, current) : undefined;
808
+ if (
809
+ retryStage
810
+ && retryContract?.status === 'failed'
811
+ && !retryContract.completionOutcome
812
+ && !retryContract.completionReceipt
813
+ ) {
814
+ transitionWorkContractPhase(
815
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
816
+ retryContract.workId,
817
+ {
818
+ phase: retryPhaseForFinalizationStage(retryStage),
819
+ status: 'running',
820
+ state: 'active',
821
+ summary: `Retrying previously failed Work finalization stage ${retryStage}.`,
822
+ },
823
+ );
824
+ }
825
+ const terminalOutcome = terminalCleanupOutcome(ctx, current);
826
+ if (terminalOutcome && args.cleanup !== false) {
827
+ return await reconcileTerminalCleanup(ctx, session, current, args, terminalOutcome);
828
+ }
829
+ const terminalizationOwner = assertWorkControllerOwnership(ctx, session, current, args);
830
+ if (terminalOutcome && args.cleanup === false) {
831
+ const recordedAt = new Date().toISOString();
832
+ current = withControllerLock(
833
+ ctx.controllerHome,
834
+ { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId },
835
+ `work-finalize:${current.workId}:retain-terminal-resources`,
836
+ () => {
837
+ const fresh = readWorkHandle(ctx.controllerHome, current.repositoryId, current.workId) ?? current;
838
+ return writeWorkHandle(ctx.controllerHome, {
839
+ ...fresh,
840
+ finalization: {
841
+ ...fresh.finalization,
842
+ validation: fresh.finalization.validation === 'failed' ? 'failed' : 'done',
843
+ commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
844
+ merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
845
+ branchCleanup: 'skipped',
846
+ worktreeCleanup: 'skipped',
847
+ lastError: fresh.finalization.validation === 'failed' ? fresh.finalization.lastError : undefined,
848
+ },
849
+ terminalResourceDisposition: {
850
+ mode: 'retained_by_request',
851
+ retainWorktree: fresh.managedWorktree,
852
+ retainBranch: true,
853
+ recordedAt,
854
+ },
855
+ });
856
+ },
857
+ 10_000,
858
+ );
859
+ appendWorkEvidence(
860
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
861
+ current.workContractId ?? current.workId,
862
+ {
863
+ title: 'terminal resources retained by request',
864
+ summary: `Controller explicitly retained ${current.managedWorktree ? 'the managed worktree and ' : ''}local branch after terminal Work; automatic terminal cleanup must not reclaim them.`,
865
+ detailLevel: 'summary',
866
+ },
867
+ );
868
+ releasePreparedWorkOwnership(ctx, current);
869
+ updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
870
+ activeWorkId: undefined,
871
+ activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
872
+ });
873
+ return {
874
+ work: compactHandle(current),
875
+ stages: current.finalization,
876
+ completed: true,
877
+ cleanupCompleted: false,
878
+ cleanupRetained: true,
879
+ cleanupPending: false,
880
+ idempotent: true,
881
+ };
882
+ }
883
+ if (current.state === 'cleaned') {
884
+ const terminalContract = contractFor(ctx, current);
885
+ if (
886
+ terminalContract?.status === 'failed'
887
+ && current.finalization.validation === 'failed'
888
+ && current.finalization.worktreeCleanup === 'done'
889
+ ) {
890
+ releasePreparedWorkOwnership(ctx, current);
891
+ updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
892
+ activeWorkId: undefined,
893
+ activeCheckoutId: workReturnCheckoutId(ctx, current, session.activeCheckoutId),
894
+ });
895
+ return {
896
+ idempotent: true,
897
+ work: compactHandle(current),
898
+ stages: current.finalization,
899
+ completed: false,
900
+ cleanupCompleted: true,
901
+ failurePreserved: true,
902
+ };
903
+ }
904
+ validateWorkHandle(ctx.controllerHome, current, identityFor(ctx, args), 'none', 'finalize');
905
+ return { idempotent: true, work: compactHandle(current) };
906
+ }
907
+ const identity = identityFor(ctx, args);
908
+ const requestedOutcome = args.completion_outcome === 'completed_no_change' || args.completion_outcome === 'completed_changed'
909
+ ? args.completion_outcome
910
+ : undefined;
911
+ const noChangeProof = typeof args.no_change_evidence === 'string' ? args.no_change_evidence.trim().slice(0, 1_000) : '';
912
+ if (requestedOutcome === 'completed_no_change' && (!noChangeProof || args.commit === true || args.merge === true)) {
913
+ throw new Error('WORK_NO_CHANGE_PROOF_REQUIRED: completed_no_change requires objective-specific evidence and forbids commit/merge');
914
+ }
915
+ const wants = requestedWants;
916
+ const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
917
+ ? args.target_branch.trim()
918
+ : undefined;
919
+ // With merge=false an explicitly selected Work branch is the delivery target,
920
+ // not a disposable feature branch. Keep the ref reachable for the receipt.
921
+ const retainExplicitWorkTargetBranch = !wants.merge && explicitTargetBranch === current.branch;
922
+ const deleteBranchRequested = args.delete_branch !== false && !retainExplicitWorkTargetBranch;
923
+ const noChangeFastPath = requestedOutcome === 'completed_no_change'
924
+ && !wants.commit
925
+ && !wants.merge
926
+ && current.state !== 'failed';
927
+
928
+ const transact = (label: string, update: (fresh: WorkHandleState) => WorkHandleState): WorkHandleState =>
929
+ withControllerLock(ctx.controllerHome, { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId }, `work-finalize:${current.workId}:${label}`, () => {
930
+ const fresh = readWorkHandle(ctx.controllerHome, current.repositoryId, current.workId) ?? current;
931
+ return update(fresh);
932
+ }, 10_000);
933
+
934
+ const failStage = (stage: keyof WorkFinalizationStages, reason: string): Record<string, unknown> => {
935
+ current = transact(`fail:${String(stage)}`, (fresh) => {
936
+ const finalization = { ...fresh.finalization, [stage]: 'failed', lastError: reason } as WorkFinalizationStages;
937
+ return markWorkHandleFailed(ctx.controllerHome, { ...fresh, finalization }, reason);
938
+ });
939
+ if (current.workContractId) {
940
+ if (stage === 'validation') {
941
+ updateWorkContract({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId, { status: 'failed' });
942
+ } else if (stage === 'commit' || stage === 'merge' || stage === 'branchCleanup' || stage === 'worktreeCleanup') {
943
+ transitionWorkContractPhase(
944
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
945
+ current.workContractId,
946
+ {
947
+ phase: retryPhaseForFinalizationStage(stage),
948
+ status: 'blocked',
949
+ state: 'blocked',
950
+ summary: `Retryable Work finalization stage ${stage} failed: ${reason}`,
951
+ },
952
+ );
953
+ }
954
+ }
955
+ markRepositoryProjectionDirty(ctx.controllerHome, current.repositoryId, `finalize:${current.workId}:${String(stage)}-failed`);
956
+ return { work: compactHandle(current), stages: current.finalization, completed: false };
957
+ };
958
+
959
+ const failedCleanupRequested = current.state === 'failed'
960
+ && wants.cleanup
961
+ && !wants.commit
962
+ && !wants.merge
963
+ && retryStage === undefined;
964
+ const failedCleanupProof = failedCleanupRequested
965
+ ? failedCleanupOnlyHead(ctx, current, args)
966
+ : undefined;
967
+ if (failedCleanupRequested && !failedCleanupProof) {
968
+ 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');
969
+ }
970
+
971
+ if (!failedCleanupRequested && !noChangeFastPath) {
972
+ current = transact('begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
973
+ ...fresh,
974
+ state: fresh.state === 'failed' ? retryHandleStateForFinalization(fresh.finalization) : fresh.state,
975
+ failureReason: undefined,
976
+ finalization: resetFinalizationStagesForRequest(fresh.finalization, wants, {
977
+ managedWorktree: fresh.managedWorktree,
978
+ deleteBranchRequested,
979
+ retainedByRequest: fresh.terminalResourceDisposition?.mode === 'retained_by_request',
980
+ }),
981
+ ...(wants.cleanup && fresh.terminalResourceDisposition?.mode === 'retained_by_request' ? { terminalResourceDisposition: undefined } : {}),
982
+ }));
983
+ }
984
+
985
+ const approvalRequestId = typeof args.approval_request_id === 'string' ? args.approval_request_id.trim() : '';
986
+ const resolvedAuthorization = approvalRequestId
987
+ ? assertResolvedAuthorization({ controllerHome: ctx.controllerHome, repositoryId: current.repositoryId, approvalRequestId, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, permissionSnapshotVersion: current.permissionSnapshotVersion, command: 'work_finalize' })
988
+ : undefined;
989
+ const gitAuthorization = resolvedAuthorization
990
+ ? { decision: 'allow', source: 'user_confirmation', reason: 'The user resolved the exact finalization approval request.' } as const
991
+ : decideAuthorization({
992
+ controllerHome: ctx.controllerHome,
993
+ accessMode: readRepositoryAccessPolicy(ctx.controllerHome, current.repositoryId).mode,
994
+ risk: 'local_git',
995
+ repositoryId: current.repositoryId,
996
+ currentRepositoryId: current.repositoryId,
997
+ workId: current.workId,
998
+ boundWorkId: current.workId,
999
+ goalId: current.goalId,
1000
+ boundGoalId: current.goalId,
1001
+ sessionId: session.sessionId,
1002
+ principalId: session.principalId,
1003
+ permissionSnapshotVersion: current.permissionSnapshotVersion,
1004
+ delegation: session.goalDelegation,
1005
+ command: 'work_finalize',
1006
+ });
1007
+ if (gitAuthorization.decision !== 'allow') return { authorization: gitAuthorization, work: compactHandle(current), stages: current.finalization };
1008
+
1009
+ // A direct Work can share canonical main with another Work's preserved dirty
1010
+ // delta. If main advanced after this Work was prepared, keep the global
1011
+ // execution-identity fence strict and reconcile only here, where delivery
1012
+ // ownership plus exact current verification can be proven together.
1013
+ if (wants.commit && !current.managedWorktree && current.expectedHead) {
1014
+ const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
1015
+ const worktree = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
1016
+ const target = selectWorkFinalizationTarget(repository, current);
1017
+ const targetBranch = explicitTargetBranch ?? repository.defaultBranch ?? 'main';
1018
+ const contract = contractFor(ctx, current);
1019
+ const advance = inspectDirectCanonicalTargetAdvanceReconciliation({
1020
+ root: target.canonicalRoot,
1021
+ worktreePath: current.worktreePath,
1022
+ managedWorktree: current.managedWorktree,
1023
+ workBranch: current.branch,
1024
+ targetBranch,
1025
+ expectedRevision: current.expectedHead,
1026
+ status: repositoryGitStatus(worktree),
1027
+ scope: contract ? { allowedPaths: contract.allowedPaths, forbiddenPaths: contract.forbiddenPaths } : undefined,
1028
+ checkIds: contract?.checks ?? [],
1029
+ checkRefs: contract?.checkRefs ?? [],
1030
+ });
1031
+ if (advance.reconcilable && advance.targetHead) {
1032
+ const previousHead = current.expectedHead;
1033
+ current = transact('direct-canonical-target-advance-reconciled', (fresh) => writeWorkHandle(ctx.controllerHome, {
1034
+ ...fresh,
1035
+ deliveryBaseCommit: advance.targetHead,
1036
+ expectedHead: advance.targetHead,
1037
+ failureReason: undefined,
1038
+ }));
1039
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1040
+ title: 'direct canonical target advancement reconciled',
1041
+ summary: `Canonical ${targetBranch} advanced linearly ${previousHead} -> ${advance.targetHead}. ${advance.targetChangedPaths.length} target-only path(s) were disjoint from all ${advance.dirtyPaths.length} positively Work-owned dirty path(s), and fresh exact verification bound [${advance.freshCheckIds.join(', ')}] to source ${advance.targetHead} plus workspace ${advance.workspaceFingerprint?.slice(0, 16) ?? 'unknown'}. Delivery identity advanced without adopting target-only commits or unrelated dirty paths.`,
1042
+ detailLevel: 'summary',
1043
+ });
1044
+ }
1045
+ }
1046
+
1047
+ if (failedCleanupProof) {
1048
+ const preservedFailure = current.failureReason ?? current.finalization.lastError ?? 'Work validation failed.';
1049
+ current = transact('failed-cleanup-begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
1050
+ ...fresh,
1051
+ state: 'failed',
1052
+ failureReason: preservedFailure,
1053
+ finalization: {
1054
+ ...fresh.finalization,
1055
+ validation: 'failed',
1056
+ commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1057
+ merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
1058
+ branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
1059
+ lastError: preservedFailure,
1060
+ },
1061
+ }));
1062
+
1063
+ if (!failedCleanupProof.worktreeMissing && current.finalization.worktreeCleanup !== 'done') {
1064
+ const target = selectWorkFinalizationTarget(
1065
+ getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
1066
+ current,
1067
+ );
1068
+ const cleanup = runCleanup(target.canonicalRoot, current.worktreePath);
1069
+ if (!cleanup.ok) {
1070
+ throw new Error(`WORK_FAILED_CLEANUP_UNSAFE: ${cleanup.message ?? 'managed worktree cleanup failed'}`);
1071
+ }
1072
+ current = transact('failed-worktree-cleanup-done', (fresh) => {
1073
+ setRepositoryCheckoutLifecycle({
1074
+ controllerHome: ctx.controllerHome,
1075
+ repoId: fresh.repositoryId,
1076
+ checkoutId: fresh.checkoutId,
1077
+ lifecycle: 'removed',
1078
+ reason: `Failed Work ${fresh.workId} cleanup completed without changing delivery state.`,
1079
+ });
1080
+ markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:failed-worktree`);
1081
+ return writeWorkHandle(ctx.controllerHome, {
1082
+ ...fresh,
1083
+ state: 'failed',
1084
+ failureReason: preservedFailure,
1085
+ finalization: { ...fresh.finalization, worktreeCleanup: 'done', lastError: preservedFailure },
1086
+ });
1087
+ });
1088
+ }
1089
+
1090
+ if (deleteBranchRequested && current.finalization.branchCleanup !== 'done') {
1091
+ const target = selectWorkFinalizationTarget(
1092
+ getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
1093
+ current,
1094
+ );
1095
+ const deleted = repositoryGitDeleteBranch(ctx.controllerHome, target, {
1096
+ branch: current.branch,
1097
+ force: false,
1098
+ authorizationDecision: gitAuthorization,
1099
+ sessionId: session.sessionId,
1100
+ principalId: session.principalId,
1101
+ workId: current.workId,
1102
+ goalId: current.goalId,
1103
+ });
1104
+ if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') {
1105
+ return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
1106
+ }
1107
+ if (deleted.execution.status !== 'executed' || deleted.execution.ok !== true) {
1108
+ current = transact('failed-branch-cleanup-failed', (fresh) => writeWorkHandle(ctx.controllerHome, {
1109
+ ...fresh,
1110
+ state: 'failed',
1111
+ failureReason: preservedFailure,
1112
+ finalization: {
1113
+ ...fresh.finalization,
1114
+ branchCleanup: 'failed',
1115
+ lastError: String(deleted.execution.stderr || 'feature branch cleanup failed').slice(0, 1_000),
1116
+ },
1117
+ }));
1118
+ return {
1119
+ work: compactHandle(current),
1120
+ stages: current.finalization,
1121
+ completed: false,
1122
+ cleanupCompleted: false,
1123
+ failurePreserved: true,
1124
+ };
1125
+ }
1126
+ current = transact('failed-branch-cleanup-done', (fresh) => {
1127
+ markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:failed-branch`);
1128
+ return writeWorkHandle(ctx.controllerHome, {
1129
+ ...fresh,
1130
+ state: 'failed',
1131
+ failureReason: preservedFailure,
1132
+ finalization: { ...fresh.finalization, branchCleanup: 'done', lastError: preservedFailure },
1133
+ });
1134
+ });
1135
+ }
1136
+
1137
+ current = transact('failed-cleanup-complete', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'cleaned', {
1138
+ failureReason: preservedFailure,
1139
+ finalization: {
1140
+ ...fresh.finalization,
1141
+ validation: 'failed',
1142
+ commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
1143
+ merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
1144
+ branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
1145
+ worktreeCleanup: 'done',
1146
+ lastError: preservedFailure,
1147
+ },
1148
+ }));
1149
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1150
+ title: 'failed work cleanup completed',
1151
+ 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}.`,
1152
+ detailLevel: 'summary',
1153
+ });
1154
+ releasePreparedWorkOwnership(ctx, current);
1155
+ updateExecutionSession(ctx.controllerHome, identity, {
1156
+ activeWorkId: undefined,
1157
+ activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
1158
+ });
1159
+ return {
1160
+ work: compactHandle(current),
1161
+ stages: current.finalization,
1162
+ completed: false,
1163
+ cleanupCompleted: true,
1164
+ failurePreserved: true,
1165
+ idempotent: false,
1166
+ };
1167
+ }
1168
+
1169
+ const contractAtStart = contractFor(ctx, current);
1170
+ const cancelledCleanupRequested = contractAtStart?.status === 'cancelled'
1171
+ && wants.cleanup
1172
+ && !wants.commit
1173
+ && !wants.merge;
1174
+ const cleanupReconciliation = requestedOutcome === 'completed_no_change'
1175
+ ? undefined
1176
+ : inspectCleanupOnlyMergedHead(ctx, current, args);
1177
+ if (cancelledCleanupRequested && !cleanupReconciliation) {
1178
+ 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');
1179
+ }
1180
+ if (cleanupReconciliation) {
1181
+ current = transact('cleanup-reconcile-merged-head', (fresh) => writeWorkHandle(ctx.controllerHome, {
1182
+ ...fresh,
1183
+ expectedHead: cleanupReconciliation.currentHead,
1184
+ state: 'merged',
1185
+ failureReason: undefined,
1186
+ finalization: {
1187
+ ...fresh.finalization,
1188
+ validation: cleanupReconciliation.worktreeMissing ? 'done' : fresh.finalization.validation,
1189
+ // Exact target containment proves an already-committed candidate. A
1190
+ // previous GIT_NOTHING_STAGED failure is superseded rather than kept as
1191
+ // a permanent failed stage after successful delivery.
1192
+ commit: fresh.finalization.commit === 'done' ? 'done' : 'skipped',
1193
+ merge: 'done',
1194
+ branchCleanup: deleteBranchRequested
1195
+ ? (fresh.finalization.branchCleanup === 'done' ? 'done' : 'pending')
1196
+ : 'skipped',
1197
+ lastError: undefined,
1198
+ },
1199
+ }));
1200
+ }
1201
+
1202
+ const changedCleanupOnly = wants.cleanup
1203
+ && !wants.commit
1204
+ && !wants.merge
1205
+ && requestedOutcome !== 'completed_no_change'
1206
+ && current.state !== 'merged'
1207
+ && current.state !== 'cleaned';
1208
+ if (changedCleanupOnly && !cleanupReconciliation) {
1209
+ 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');
1210
+ }
1211
+
1212
+ const terminalCleanupOnly = Boolean(cleanupReconciliation && wants.cleanup && !wants.commit && !wants.merge);
1213
+ let exactValidationInput: ReturnType<typeof currentWorkValidationInput> | undefined;
1214
+ if (terminalCleanupOnly) {
1215
+ current = transact('terminal-cleanup-validation-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
1216
+ ...fresh,
1217
+ finalization: { ...fresh.finalization, validation: 'done', lastError: undefined },
1218
+ }));
1219
+ } else {
1220
+ let validatedRepository: RepositoryRecord;
1221
+ try {
1222
+ validatedRepository = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize').worktreeRepository;
1223
+ } catch (error) {
1224
+ return failStage('validation', error instanceof Error ? error.message : String(error));
1225
+ }
1226
+ const validationContract = contractFor(ctx, current);
1227
+ if (!validationContract) throw new Error(`WORK_VALIDATION_CONTRACT_MISSING: ${current.workContractId ?? current.workId}`);
1228
+ const validationInput = currentWorkValidationInput(validatedRepository, current, validationContract.checks);
1229
+ exactValidationInput = validationInput;
1230
+ if (validationContract.checks.length === 0) {
1231
+ if (!noChangeFastPath) {
1232
+ current = transact('validation-no-checks', (fresh) => writeWorkHandle(ctx.controllerHome, {
1233
+ ...fresh,
1234
+ validatedInputFingerprint: validationInput.fingerprint,
1235
+ finalization: { ...fresh.finalization, validation: 'done', lastError: undefined },
1236
+ }));
1237
+ }
1238
+ projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required.');
1239
+ } else if (!hasCurrentWorkValidationAuthority({
1240
+ finalizationValidation: current.finalization.validation,
1241
+ validatedInputFingerprint: current.validatedInputFingerprint,
1242
+ evidenceState: validationContract.evidenceState,
1243
+ expectedFingerprint: validationInput.fingerprint,
1244
+ })) {
1245
+ current = transact('validation-required', (fresh) => writeWorkHandle(ctx.controllerHome, {
1246
+ ...fresh,
1247
+ validatedInputFingerprint: undefined,
1248
+ finalization: { ...fresh.finalization, validation: 'pending', lastError: undefined },
1249
+ }));
1250
+ markWorkValidationPending(ctx.controllerHome, current);
1251
+ throw new Error('WORK_VALIDATION_REQUIRED: run work_validate against the exact current workspace before finalization');
1252
+ }
1253
+ if (!validationInput.clean && wants.commit && current.finalization.commit === 'done') {
1254
+ current = transact('rearm-delivery-after-validated-repair', (fresh) => writeWorkHandle(ctx.controllerHome, {
1255
+ ...fresh,
1256
+ state: 'validating',
1257
+ failureReason: undefined,
1258
+ finalization: resetFinalizationStagesForRequest(fresh.finalization, wants, {
1259
+ managedWorktree: fresh.managedWorktree,
1260
+ deleteBranchRequested,
1261
+ retainedByRequest: fresh.terminalResourceDisposition?.mode === 'retained_by_request',
1262
+ workspaceDirty: true,
1263
+ }),
1264
+ }));
1265
+ }
1266
+ if (wants.merge && !wants.commit && !validationInput.clean) {
1267
+ throw new Error('WORK_MERGE_UNCOMMITTED_CHANGES: commit the validated workspace before merging');
1268
+ }
1269
+ }
1270
+
1271
+ if (noChangeFastPath) {
1272
+ if (!exactValidationInput?.clean) {
1273
+ throw new Error('WORK_NO_CHANGE_DIRTY: completed_no_change cannot retain an owned dirty worktree');
1274
+ }
1275
+ const noChangeFinalization: WorkFinalizationStages = {
1276
+ ...current.finalization,
1277
+ validation: 'done',
1278
+ commit: 'skipped',
1279
+ merge: 'skipped',
1280
+ branchCleanup: wants.cleanup && deleteBranchRequested ? 'pending' : 'skipped',
1281
+ lastError: undefined,
1282
+ };
1283
+ // Collapse the no-op Git stages into the same durable lifecycle write used
1284
+ // to enter/establish the delivery boundary. Physical cleanup remains
1285
+ // separately persisted because it has real side effects and crash semantics.
1286
+ if (current.state === 'prepared') {
1287
+ current = transact('no-change-validated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'validating', {
1288
+ finalization: noChangeFinalization,
1289
+ validatedInputFingerprint: exactValidationInput.fingerprint,
1290
+ failureReason: undefined,
1291
+ }));
1292
+ } else if (current.state === 'editing' || current.state === 'validating' || current.state === 'committed') {
1293
+ current = transact('no-change-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
1294
+ finalization: noChangeFinalization,
1295
+ validatedInputFingerprint: exactValidationInput.fingerprint,
1296
+ failureReason: undefined,
1297
+ }));
1298
+ }
1299
+ }
1300
+
1301
+ if (wants.commit && current.finalization.commit === 'pending') {
1302
+ const validated = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
1303
+ const contract = contractFor(ctx, current);
1304
+ if (contract?.constraints.allowCommit === false) throw new Error('WORK_COMMIT_NOT_ALLOWED: WorkContract disallows commit');
1305
+ const status = repositoryGitStatus(validated.worktreeRepository);
1306
+ const { dirtyPaths, ownedPaths: commitPaths } = workOwnedDirtyPaths(contract, status);
1307
+ if (dirtyPaths.length > 0 && commitPaths.length === 0) {
1308
+ 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`);
1309
+ }
1310
+ 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 });
1311
+ const pendingAuthorization = [committed.stage, committed.commit].find((execution) => execution?.authorizationDecision?.decision === 'user_confirmation_required')?.authorizationDecision;
1312
+ if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
1313
+ if (!committed.committed) return { ...failStage('commit', committed.error?.message ?? 'commit failed'), commit: committed };
1314
+ const checks = contract?.checks ?? [];
1315
+ const committedValidatedWorkspaceFingerprint = exactValidationInput && committed.after.clean
1316
+ ? workspaceValidationFingerprint(validated.worktreeRepository.canonicalRoot, committed.before)
1317
+ : undefined;
1318
+ const validationPreservedAcrossCommit = Boolean(
1319
+ checks.length > 0
1320
+ && exactValidationInput
1321
+ && committedValidatedWorkspaceFingerprint === exactValidationInput.workspaceFingerprint
1322
+ );
1323
+ const committedHead = gitHead(validated.worktreeRepository.canonicalRoot);
1324
+ const postCommitInput = currentWorkValidationInput(validated.worktreeRepository, { ...current, expectedHead: committedHead }, checks);
1325
+ current = transact('commit-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'committed', {
1326
+ expectedHead: committedHead,
1327
+ finalization: {
1328
+ ...fresh.finalization,
1329
+ validation: checks.length ? (validationPreservedAcrossCommit ? 'done' : 'pending') : 'done',
1330
+ commit: 'done',
1331
+ lastError: undefined,
1332
+ },
1333
+ validatedInputFingerprint: checks.length
1334
+ ? (validationPreservedAcrossCommit ? postCommitInput.fingerprint : undefined)
1335
+ : postCommitInput.fingerprint,
1336
+ failureReason: undefined,
1337
+ }));
1338
+ if (checks.length && !validationPreservedAcrossCommit) {
1339
+ markWorkValidationPending(ctx.controllerHome, current);
1340
+ return {
1341
+ work: compactHandle(current),
1342
+ stages: current.finalization,
1343
+ completed: false,
1344
+ continuation: 'WORK_COMMITTED_REVALIDATION_REQUIRED: committed content changed from the exact validated workspace; run work_validate on the committed HEAD before merge or completion',
1345
+ };
1346
+ }
1347
+ if (validationPreservedAcrossCommit && exactValidationInput) {
1348
+ const checkDefinitions = listControllerChecks(validated.worktreeRepository.canonicalRoot);
1349
+ for (const checkId of checks) {
1350
+ const check = checkDefinitions.find((entry) => entry.id === checkId);
1351
+ if (check?.effects?.git !== undefined) continue;
1352
+ const sourcePass = contract
1353
+ ? effectiveVerificationEvidence(contract.checkRefs, {
1354
+ sourceRevision: exactValidationInput.head,
1355
+ workspaceFingerprint: exactValidationInput.workspaceFingerprint,
1356
+ checkId,
1357
+ requestedChecks: checks,
1358
+ }).find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))?.record
1359
+ : undefined;
1360
+ if (!sourcePass) continue;
1361
+ appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1362
+ ...sourcePass,
1363
+ summary: `Validation authority transferred across content-equivalent commit for ${checkId}.`,
1364
+ recordedAt: new Date().toISOString(),
1365
+ sourceRevision: postCommitInput.head,
1366
+ workspaceFingerprint: postCommitInput.workspaceFingerprint,
1367
+ verificationInputFingerprint: verificationInputFingerprint({
1368
+ sourceRevision: postCommitInput.head,
1369
+ workspaceFingerprint: postCommitInput.workspaceFingerprint,
1370
+ checkId,
1371
+ requestedChecks: checks,
1372
+ }),
1373
+ evidenceRef: {
1374
+ title: checkId,
1375
+ summary: 'Reused prior valid receipt after proving the validated filesystem content was committed without drift.',
1376
+ detailLevel: 'summary',
1377
+ },
1378
+ });
1379
+ }
1380
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1381
+ title: 'validation authority preserved across content-equivalent commit',
1382
+ 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}.`,
1383
+ detailLevel: 'summary',
1384
+ });
1385
+ } else if (checks.length === 0) {
1386
+ projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required after commit.');
1387
+ }
1388
+ } else if (!wants.commit && current.finalization.commit === 'pending') {
1389
+ current = transact('commit-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, commit: 'skipped' } }));
1390
+ }
1391
+
1392
+ if (wants.merge && current.finalization.merge === 'pending' && current.finalization.commit === 'done') {
1393
+ const repository = getRepository(current.repositoryId, ctx.controllerHome);
1394
+ const target = selectWorkFinalizationTarget(repository, current);
1395
+ const targetBranch = explicitTargetBranch ?? repository.defaultBranch ?? 'main';
1396
+ const directTarget = inspectDirectTargetDelivery(
1397
+ target.canonicalRoot,
1398
+ current.worktreePath,
1399
+ current.managedWorktree,
1400
+ current.branch,
1401
+ targetBranch,
1402
+ current.expectedHead,
1403
+ );
1404
+ if (directTarget.integrated) {
1405
+ current = transact('direct-target-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
1406
+ finalization: { ...fresh.finalization, merge: 'done', branchCleanup: 'skipped', lastError: undefined },
1407
+ failureReason: undefined,
1408
+ }));
1409
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1410
+ title: 'direct target delivery already integrated',
1411
+ 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.`,
1412
+ detailLevel: 'summary',
1413
+ });
1414
+ }
1415
+ }
1416
+
1417
+ if (wants.merge && current.finalization.merge === 'pending') {
1418
+ const mergeValidated = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
1419
+ if (mergeValidated.currentHead && mergeValidated.currentHead !== current.expectedHead) {
1420
+ current = transact('merge-adopt-work-delivery-head', (fresh) => writeWorkHandle(ctx.controllerHome, {
1421
+ ...fresh,
1422
+ expectedHead: mergeValidated.currentHead,
1423
+ failureReason: undefined,
1424
+ }));
1425
+ }
1426
+ const contract = contractFor(ctx, current);
1427
+ if (contract?.constraints.allowMerge === false) throw new Error('WORK_MERGE_NOT_ALLOWED: WorkContract disallows merge');
1428
+ const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
1429
+ const deleteAfterWorktreeCleanup = current.managedWorktree && deleteBranchRequested;
1430
+ const targetBranch = explicitTargetBranch ?? target.defaultBranch ?? 'main';
1431
+ const activeMergeHead = retryStage === 'merge' ? gitRevision(target.canonicalRoot, 'MERGE_HEAD') : undefined;
1432
+ if (!activeMergeHead && current.managedWorktree && current.expectedHead) {
1433
+ const targetHead = gitRevision(target.canonicalRoot, targetBranch);
1434
+ if (targetHead) {
1435
+ const advance = inspectWorkTargetAdvance(mergeValidated.worktreeRepository.canonicalRoot, current.expectedHead, targetHead);
1436
+ if (advance.relation === 'diverged_conflict') {
1437
+ if (contract) {
1438
+ transitionWorkContractPhase(
1439
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1440
+ contract.workId,
1441
+ {
1442
+ phase: 'delivery',
1443
+ status: 'blocked',
1444
+ state: 'blocked',
1445
+ summary: `Target branch ${targetBranch} advanced to ${advance.targetHead} and conflicts with candidate ${advance.candidateHead}; canonical target was not mutated.`,
1446
+ },
1447
+ );
1448
+ }
1449
+ return {
1450
+ work: compactHandle(current),
1451
+ stages: current.finalization,
1452
+ completed: false,
1453
+ blocked: true,
1454
+ recoverable: true,
1455
+ error: {
1456
+ code: 'WORK_TARGET_ADVANCE_CONFLICT',
1457
+ message: `Target branch ${targetBranch} advanced and cannot be integrated cleanly into the isolated Work candidate. Canonical target was left unchanged.${advance.detail ? ` ${advance.detail}` : ''}`,
1458
+ },
1459
+ continuation: 'WORK_TARGET_ADVANCE_REVIEW_REQUIRED: inspect the target/candidate conflict, repair only inside the managed Work checkout, then revalidate before retrying finalize.',
1460
+ };
1461
+ }
1462
+ if (advance.relation === 'candidate_contains_target') {
1463
+ const nonLinearCommits = targetAdvanceLinearMergeCommits(
1464
+ mergeValidated.worktreeRepository.canonicalRoot,
1465
+ advance.targetHead,
1466
+ advance.candidateHead,
1467
+ );
1468
+ if (nonLinearCommits.length > 0) {
1469
+ 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(', ')}`);
1470
+ }
1471
+ const scopeViolation = targetAdvanceWorkScopeViolation(contract, advance.candidateChangedPaths);
1472
+ if (scopeViolation) {
1473
+ return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: Work-owned ${scopeViolation.kind} path ${scopeViolation.path}`);
1474
+ }
1475
+ if (contract) replaceTargetAdvanceScopeEvidence(ctx, contract, advance.candidateChangedPaths);
1476
+ current = transact('target-advance-provenance-confirmed', (fresh) => writeWorkHandle(ctx.controllerHome, {
1477
+ ...fresh,
1478
+ deliveryBaseCommit: advance.targetHead,
1479
+ }));
1480
+ }
1481
+ if (advance.relation === 'diverged_clean') {
1482
+ const preIntegrationScopeViolation = targetAdvanceWorkScopeViolation(contract, advance.candidateChangedPaths);
1483
+ if (preIntegrationScopeViolation) {
1484
+ return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: Work-owned ${preIntegrationScopeViolation.kind} path ${preIntegrationScopeViolation.path}`);
1485
+ }
1486
+ const checks = contract?.checks ?? [];
1487
+ const candidateInput = currentWorkValidationInput(mergeValidated.worktreeRepository, current, checks);
1488
+ const checksBefore = checks.length > 0 ? listControllerChecks(mergeValidated.worktreeRepository.canonicalRoot) : [];
1489
+ const integrated = repositoryGitRebaseOnto(ctx.controllerHome, mergeValidated.worktreeRepository, {
1490
+ onto: advance.targetHead,
1491
+ upstream: advance.mergeBase,
1492
+ abortOnFailure: true,
1493
+ authorizationDecision: gitAuthorization,
1494
+ sessionId: session.sessionId,
1495
+ principalId: session.principalId,
1496
+ workId: current.workId,
1497
+ goalId: current.goalId,
1498
+ });
1499
+ if (!integrated.rebased) {
1500
+ if (contract) {
1501
+ transitionWorkContractPhase(
1502
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1503
+ contract.workId,
1504
+ {
1505
+ phase: 'delivery',
1506
+ status: 'blocked',
1507
+ state: 'blocked',
1508
+ summary: `Linear target-advance integration failed inside the isolated Work checkout; canonical ${targetBranch} was not mutated.`,
1509
+ },
1510
+ );
1511
+ }
1512
+ return {
1513
+ work: compactHandle(current),
1514
+ stages: current.finalization,
1515
+ completed: false,
1516
+ blocked: true,
1517
+ recoverable: integrated.restored,
1518
+ error: {
1519
+ code: 'WORK_TARGET_ADVANCE_LINEAR_INTEGRATION_FAILED',
1520
+ message: integrated.restored
1521
+ ? 'Target advancement could not be rebased safely; the isolated Work checkout was restored to its original candidate HEAD.'
1522
+ : 'Target advancement rebase failed and exact checkout restoration could not be proven; inspect the isolated Work checkout before retrying.',
1523
+ },
1524
+ continuation: 'WORK_TARGET_ADVANCE_REVIEW_REQUIRED: inspect the isolated Work checkout and retry only after its exact branch/HEAD/cleanliness are proven.',
1525
+ };
1526
+ }
1527
+ const integratedHead = gitHead(mergeValidated.worktreeRepository.canonicalRoot);
1528
+ if (!integratedHead || !gitIsAncestor(mergeValidated.worktreeRepository.canonicalRoot, advance.targetHead, integratedHead)) {
1529
+ return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_ANCESTRY_MISMATCH: target ${advance.targetHead} is not an ancestor of ${integratedHead ?? 'unavailable'}`);
1530
+ }
1531
+ const mergeCommits = targetAdvanceLinearMergeCommits(
1532
+ mergeValidated.worktreeRepository.canonicalRoot,
1533
+ advance.targetHead,
1534
+ integratedHead,
1535
+ );
1536
+ const integratedTree = spawnSync('git', ['-C', mergeValidated.worktreeRepository.canonicalRoot, 'rev-parse', '--verify', `${integratedHead}^{tree}`], {
1537
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
1538
+ });
1539
+ if (mergeCommits.length > 0) {
1540
+ return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_HISTORY_VIOLATION: integrated candidate contains merge commit(s): ${mergeCommits.slice(0, 8).join(', ')}`);
1541
+ }
1542
+ if (
1543
+ !advance.mergedTree
1544
+ || integratedTree.status !== 0
1545
+ || integratedTree.error
1546
+ || typeof integratedTree.stdout !== 'string'
1547
+ || integratedTree.stdout.trim() !== advance.mergedTree
1548
+ ) {
1549
+ return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_TREE_MISMATCH: rebased candidate tree does not equal the preflight clean integration tree ${advance.mergedTree ?? 'unavailable'}`);
1550
+ }
1551
+ const workOwnedChangedPaths = gitChangedPaths(mergeValidated.worktreeRepository.canonicalRoot, advance.targetHead, integratedHead);
1552
+ const postIntegrationScopeViolation = targetAdvanceWorkScopeViolation(contract, workOwnedChangedPaths);
1553
+ if (postIntegrationScopeViolation) {
1554
+ return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: integrated Work-owned ${postIntegrationScopeViolation.kind} path ${postIntegrationScopeViolation.path}`);
1555
+ }
1556
+ const integratedInput = currentWorkValidationInput(
1557
+ mergeValidated.worktreeRepository,
1558
+ { ...current, expectedHead: integratedHead },
1559
+ checks,
1560
+ );
1561
+ const checksAfter = checks.length > 0 ? listControllerChecks(mergeValidated.worktreeRepository.canonicalRoot) : [];
1562
+ const transferPlan = contract
1563
+ ? planTargetAdvanceValidationAuthority({
1564
+ checkIds: checks,
1565
+ checkRefs: contract.checkRefs,
1566
+ checksBefore,
1567
+ checksAfter,
1568
+ candidateHead: advance.candidateHead,
1569
+ candidateWorkspaceFingerprint: candidateInput.workspaceFingerprint,
1570
+ integratedHead,
1571
+ integratedWorkspaceFingerprint: integratedInput.workspaceFingerprint,
1572
+ targetChangedPaths: advance.targetChangedPaths,
1573
+ })
1574
+ : { transferredRecords: [], reusableCheckIds: [], invalidatedCheckIds: checks };
1575
+ for (const record of transferPlan.transferredRecords) {
1576
+ appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, record);
1577
+ }
1578
+ if (contract) replaceTargetAdvanceScopeEvidence(ctx, contract, workOwnedChangedPaths);
1579
+ const validationPreserved = transferPlan.invalidatedCheckIds.length === 0;
1580
+ current = transact('target-advance-integrated', (fresh) => transitionWorkHandle(
1581
+ ctx.controllerHome,
1582
+ fresh,
1583
+ validationPreserved ? 'committed' : 'validating',
1584
+ {
1585
+ deliveryBaseCommit: advance.targetHead,
1586
+ expectedHead: integratedHead,
1587
+ failureReason: undefined,
1588
+ finalization: { ...fresh.finalization, validation: validationPreserved ? 'done' : 'pending', merge: 'pending', lastError: undefined },
1589
+ validationRun: undefined,
1590
+ validatedInputFingerprint: validationPreserved ? integratedInput.fingerprint : undefined,
1591
+ },
1592
+ ));
1593
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1594
+ title: 'target advancement linearly integrated into isolated Work candidate',
1595
+ 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.`,
1596
+ detailLevel: 'summary',
1597
+ });
1598
+ if (!validationPreserved) {
1599
+ markWorkValidationPending(ctx.controllerHome, current);
1600
+ return {
1601
+ work: compactHandle(current),
1602
+ stages: current.finalization,
1603
+ completed: false,
1604
+ 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`,
1605
+ };
1606
+ }
1607
+ projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', checks.length === 0
1608
+ ? 'Target advancement was linearly integrated and no validation checks were required.'
1609
+ : `Target advancement was linearly integrated; all ${checks.length} check result(s) retained authority because target-only changes did not affect their declared inputs.`);
1610
+ }
1611
+ }
1612
+ }
1613
+ let merged: ReturnType<typeof repositoryGitFinishWorkflow>;
1614
+ if (activeMergeHead) {
1615
+ const expectedMergeHead = current.expectedHead ? gitRevision(target.canonicalRoot, current.expectedHead) : undefined;
1616
+ const featureHead = gitRevision(target.canonicalRoot, current.branch);
1617
+ const currentBranchResult = spawnSync('git', ['-C', target.canonicalRoot, 'branch', '--show-current'], {
1618
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
1619
+ });
1620
+ const currentBranchName = currentBranchResult.status === 0 && typeof currentBranchResult.stdout === 'string'
1621
+ ? currentBranchResult.stdout.trim()
1622
+ : '';
1623
+ if (!expectedMergeHead || activeMergeHead !== expectedMergeHead || featureHead !== expectedMergeHead || currentBranchName !== targetBranch) {
1624
+ return failStage(
1625
+ 'merge',
1626
+ `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'}`,
1627
+ );
1628
+ }
1629
+ const unmerged = spawnSync('git', ['-C', target.canonicalRoot, 'diff', '--name-only', '--diff-filter=U'], {
1630
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
1631
+ });
1632
+ if (unmerged.status !== 0 || unmerged.error || typeof unmerged.stdout !== 'string') {
1633
+ return failStage('merge', 'WORK_MERGE_CONFLICT_STATE_UNAVAILABLE: unable to prove that the owned merge has no unmerged paths');
1634
+ }
1635
+ if (unmerged.stdout.trim()) {
1636
+ return failStage('merge', `WORK_MERGE_CONFLICTS_UNRESOLVED: resolve and stage all paths before retrying finalize (${unmerged.stdout.trim().split('\n').filter(Boolean).join(', ')})`);
1637
+ }
1638
+ const beforeConclude = repositoryGitStatus(target);
1639
+ const concluded = repositoryGitCommit(ctx.controllerHome, target, {
1640
+ message: `Merge ${current.branch} into ${targetBranch}`,
1641
+ allowEmpty: true,
1642
+ authorizationDecision: gitAuthorization,
1643
+ sessionId: session.sessionId,
1644
+ principalId: session.principalId,
1645
+ workId: current.workId,
1646
+ goalId: current.goalId,
1647
+ });
1648
+ if (!concluded.committed || !concluded.commit) {
1649
+ return failStage('merge', concluded.error?.message ?? 'WORK_MERGE_CONCLUDE_FAILED: git commit did not conclude the owned merge');
1650
+ }
1651
+ const concludedSecondParent = gitRevision(target.canonicalRoot, 'HEAD^2');
1652
+ if (concludedSecondParent !== expectedMergeHead || gitRevision(target.canonicalRoot, 'MERGE_HEAD')) {
1653
+ return failStage('merge', `WORK_MERGE_CONCLUSION_OWNERSHIP_MISMATCH: concluded merge must retain ${expectedMergeHead} as its second parent and clear MERGE_HEAD`);
1654
+ }
1655
+ const steps: ReturnType<typeof repositoryGitFinishWorkflow>['steps'] = [{ name: 'conclude_owned_merge', execution: concluded.commit }];
1656
+ if (!deleteAfterWorktreeCleanup && deleteBranchRequested) {
1657
+ const deleted = repositoryGitDeleteBranch(ctx.controllerHome, target, { branch: current.branch, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
1658
+ steps.push({ name: 'delete_feature_branch', execution: deleted.execution });
1659
+ if (deleted.execution.status !== 'executed' || deleted.execution.ok !== true) {
1660
+ return failStage('merge', deleted.execution.stderr || 'WORK_MERGE_BRANCH_CLEANUP_FAILED: owned merge concluded but feature branch deletion failed');
1661
+ }
1662
+ }
1663
+ merged = {
1664
+ repoId: target.repoId,
1665
+ checkoutId: target.activeCheckoutId,
1666
+ featureBranch: current.branch,
1667
+ targetBranch,
1668
+ before: beforeConclude,
1669
+ steps,
1670
+ after: repositoryGitStatus(target),
1671
+ completed: true,
1672
+ };
1673
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1674
+ title: 'owned resolved merge concluded',
1675
+ 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.`,
1676
+ detailLevel: 'summary',
1677
+ });
1678
+ } else {
1679
+ const integrationLockKey = { scope: 'task' as const, repoId: current.repositoryId, taskId: `target-integration:${targetBranch}` };
1680
+ try {
1681
+ merged = withControllerLock(
1682
+ ctx.controllerHome,
1683
+ integrationLockKey,
1684
+ `work-finalize-target:${current.workId}`,
1685
+ () => {
1686
+ const lockedTargetStatus = repositoryGitStatus(target);
1687
+ const lockedDirtyPaths = [...new Set([
1688
+ ...lockedTargetStatus.staged,
1689
+ ...lockedTargetStatus.unstaged,
1690
+ ...lockedTargetStatus.untracked,
1691
+ ])].sort();
1692
+ let preserveDirtyTargetPaths: string[] | undefined;
1693
+ if (lockedDirtyPaths.length > 0) {
1694
+ const ownership = inspectTargetDirtyWorkOwnership({
1695
+ dirtyPaths: lockedDirtyPaths,
1696
+ targetCheckoutId: target.activeCheckoutId,
1697
+ currentWorkId: current.workId,
1698
+ activeWorks: listWorkContracts({ controllerHome: ctx.controllerHome, repoId: current.repositoryId, status: 'active', limit: 200 }),
1699
+ });
1700
+ if (!ownership.owned) {
1701
+ const detail = [
1702
+ ownership.unownedPaths.length ? `unowned=[${ownership.unownedPaths.join(', ')}]` : '',
1703
+ ownership.ambiguousPaths.length ? `ambiguous=[${ownership.ambiguousPaths.join(', ')}]` : '',
1704
+ ].filter(Boolean).join(' ');
1705
+ throw new Error(`WORK_TARGET_DIRTY_OWNERSHIP_REQUIRED: target ${targetBranch} has dirty path(s) that cannot be attributed uniquely to another active Work. ${detail}`.trim());
1706
+ }
1707
+ preserveDirtyTargetPaths = ownership.dirtyPaths;
1708
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
1709
+ title: 'concurrent target dirty ownership preserved',
1710
+ summary: `Target ${targetBranch} retained ${ownership.dirtyPaths.length} dirty path(s) owned by other active Work while this Work entered only the target-branch mutation critical section.`,
1711
+ detailLevel: 'summary',
1712
+ });
1713
+ }
1714
+ return repositoryGitFinishWorkflow(ctx.controllerHome, target, {
1715
+ featureBranch: current.branch,
1716
+ targetBranch: explicitTargetBranch,
1717
+ deleteBranch: !deleteAfterWorktreeCleanup && deleteBranchRequested,
1718
+ noFf: args.no_ff === true,
1719
+ preserveDirtyTargetPaths,
1720
+ authorizationDecision: gitAuthorization,
1721
+ sessionId: session.sessionId,
1722
+ principalId: session.principalId,
1723
+ workId: current.workId,
1724
+ goalId: current.goalId,
1725
+ });
1726
+ },
1727
+ 30_000,
1728
+ 5_000,
1729
+ );
1730
+ } catch (error) {
1731
+ return failStage('merge', error instanceof Error ? error.message : String(error));
1732
+ }
1733
+ }
1734
+ const pendingAuthorization = merged.steps.find((step) => step.execution.authorizationDecision?.decision === 'user_confirmation_required')?.execution.authorizationDecision;
1735
+ if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
1736
+ if (!merged.completed) return { ...failStage('merge', merged.error?.message ?? 'merge failed'), merge: merged };
1737
+ current = transact('merge-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
1738
+ finalization: { ...fresh.finalization, merge: 'done', branchCleanup: !deleteBranchRequested ? 'skipped' : deleteAfterWorktreeCleanup ? 'pending' : 'done', lastError: undefined },
1739
+ failureReason: undefined,
1740
+ }));
1741
+ } else if (!wants.merge && current.finalization.merge === 'pending') {
1742
+ current = transact('merge-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
1743
+ ...fresh,
1744
+ finalization: {
1745
+ ...fresh.finalization,
1746
+ merge: 'skipped',
1747
+ branchCleanup: requestedOutcome === 'completed_no_change' && wants.cleanup && deleteBranchRequested ? 'pending' : 'skipped',
1748
+ },
1749
+ }));
1750
+ }
1751
+
1752
+ let remoteDelivery: WorkRemoteDeliveryReceipt | undefined;
1753
+ const remoteDeliveryRequired = args.remote_write === true
1754
+ || contractFor(ctx, current)?.constraints.remoteDeliveryRequired === true;
1755
+ if (remoteDeliveryRequired && requestedOutcome !== 'completed_no_change') {
1756
+ const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
1757
+ const target = selectWorkFinalizationTarget(repository, current);
1758
+ const targetBranch = explicitTargetBranch ?? repository.defaultBranch ?? 'main';
1759
+ const targetRevision = gitRevision(target.canonicalRoot, targetBranch);
1760
+ if (!targetRevision) throw new Error(`WORK_REMOTE_DELIVERY_TARGET_REQUIRED: target branch ${targetBranch} is unavailable`);
1761
+ const authorityRepository = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
1762
+ remoteDelivery = await pushExactWorkRemoteDelivery({
1763
+ controllerHome: ctx.controllerHome,
1764
+ repository: authorityRepository,
1765
+ workId: current.workId,
1766
+ targetBranch,
1767
+ targetRevision,
1768
+ });
1769
+ appendWorkEvidence(
1770
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1771
+ current.workContractId ?? current.workId,
1772
+ {
1773
+ title: 'remote delivery verified before Work terminalization',
1774
+ summary: remoteDelivery.pushed
1775
+ ? `Exact integrated revision ${remoteDelivery.targetRevision} was pushed to origin/${targetBranch} while Work ${current.workId} still held active delivery authority.`
1776
+ : `origin/${targetBranch} already contained exact integrated revision ${remoteDelivery.targetRevision}; remote delivery was verified idempotently before Work terminalization.`,
1777
+ detailLevel: 'summary',
1778
+ },
1779
+ );
1780
+ }
1781
+
1782
+ if (
1783
+ requestedOutcome === 'completed_no_change'
1784
+ && current.finalization.validation === 'done'
1785
+ && current.finalization.commit === 'skipped'
1786
+ && current.finalization.merge === 'skipped'
1787
+ && current.state !== 'merged'
1788
+ && current.state !== 'cleaned'
1789
+ ) {
1790
+ // `merged` is the WorkHandle's durable integrated-delivery boundary. A
1791
+ // no-change completion has no Git merge, but it must cross that boundary
1792
+ // before physical cleanup so an earlier failure preserves retryability.
1793
+ if (current.state === 'prepared') {
1794
+ current = transact('no-change-validation-state', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'validating', {
1795
+ finalization: fresh.finalization,
1796
+ failureReason: undefined,
1797
+ }));
1798
+ }
1799
+ if (current.state === 'editing' || current.state === 'validating' || current.state === 'committed') {
1800
+ current = transact('no-change-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
1801
+ finalization: fresh.finalization,
1802
+ failureReason: undefined,
1803
+ }));
1804
+ } else if (current.state !== 'merged') {
1805
+ throw new Error(`WORK_NO_CHANGE_DELIVERY_STATE_INVALID: cannot establish no-change delivery from ${current.state}`);
1806
+ }
1807
+ }
1808
+
1809
+ if (wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
1810
+ const contract = contractFor(ctx, current);
1811
+ if (contract?.constraints.allowCleanup === false) throw new Error('WORK_CLEANUP_NOT_ALLOWED: WorkContract disallows cleanup');
1812
+ if (!current.managedWorktree) {
1813
+ current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
1814
+ } else {
1815
+ const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
1816
+ const cleanup = runCleanup(target.canonicalRoot, current.worktreePath);
1817
+ if (!cleanup.ok) return failStage('worktreeCleanup', cleanup.message ?? 'worktree cleanup failed');
1818
+ current = transact('worktree-cleanup-done', (fresh) => {
1819
+ setRepositoryCheckoutLifecycle({ controllerHome: ctx.controllerHome, repoId: fresh.repositoryId, checkoutId: fresh.checkoutId, lifecycle: 'removed', reason: `Work ${fresh.workId} cleanup completed.` });
1820
+ markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:worktree`);
1821
+ return writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'done', lastError: undefined } });
1822
+ });
1823
+ }
1824
+ } else if (!wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
1825
+ current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
1826
+ }
1827
+
1828
+ if (
1829
+ wants.cleanup
1830
+ && current.finalization.branchCleanup === 'pending'
1831
+ && (current.finalization.merge === 'done' || requestedOutcome === 'completed_no_change')
1832
+ ) {
1833
+ const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
1834
+ 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 });
1835
+ if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
1836
+ if (deleted.execution.status !== 'executed' || deleted.execution.ok !== true) return failStage('branchCleanup', deleted.execution.stderr || 'feature branch cleanup failed');
1837
+ current = transact('branch-cleanup-done', (fresh) => {
1838
+ markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:branch`);
1839
+ return writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, branchCleanup: 'done', lastError: undefined } });
1840
+ });
1841
+ }
1842
+
1843
+ const complete = finalizationComplete(current.finalization);
1844
+ if (complete) {
1845
+ const finalState = finalStateForStages(current.finalization, current.state);
1846
+ current = transact('complete', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, finalState, { finalization: current.finalization, failureReason: undefined }));
1847
+ const completedContract = contractFor(ctx, current);
1848
+ if (completedContract?.status === 'cancelled') {
1849
+ appendWorkEvidence(
1850
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1851
+ completedContract.workId,
1852
+ {
1853
+ title: 'cancelled work cleanup completed',
1854
+ summary: 'Controller verified terminal ownership, a clean or previously removed managed worktree, and a branch HEAD already contained in the target branch before deleting retained workspace references.',
1855
+ detailLevel: 'summary',
1856
+ },
1857
+ );
1858
+ } else {
1859
+ const workId = current.workContractId ?? current.workId;
1860
+ const completionContract = contractFor(ctx, current);
1861
+ if (!completionContract) throw new Error(`WORK_COMPLETION_CONTRACT_MISSING: ${workId}`);
1862
+ if (requestedOutcome === 'completed_no_change') {
1863
+ appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, workId, {
1864
+ title: 'objective-specific no-change proof',
1865
+ summary: noChangeProof,
1866
+ detailLevel: 'summary',
1867
+ });
1868
+ }
1869
+ const receipt = completionReceiptForFinalizedWork(ctx, current, completionContract, args);
1870
+ const ownerPrincipal = terminalizationOwner.principalId?.trim() || terminalizationOwner.controllerId;
1871
+ const ownerInstanceId = terminalizationOwner.controllerInstanceId?.trim() || '';
1872
+ const ownerClaimGeneration = terminalizationOwner.claimGeneration;
1873
+ if (!ownerInstanceId || typeof ownerClaimGeneration !== 'number' || ownerClaimGeneration < 1) {
1874
+ throw new Error(`WORK_CONTROLLER_TERMINALIZATION_AUTHORITY_INVALID: ${workId}`);
1875
+ }
1876
+ const fencedCompletion = withControllerSessionTerminalizationFence(
1877
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1878
+ {
1879
+ workId,
1880
+ actor: `work-finalize-completion:${terminalizationOwner.controllerId}:${ownerInstanceId}`,
1881
+ authority: {
1882
+ controllerId: terminalizationOwner.controllerId,
1883
+ controllerType: terminalizationOwner.controllerType,
1884
+ principalId: ownerPrincipal,
1885
+ controllerInstanceId: ownerInstanceId,
1886
+ claimGeneration: ownerClaimGeneration,
1887
+ },
1888
+ },
1889
+ () => completeWorkWithReceipt(
1890
+ { controllerHome: ctx.controllerHome, repoId: current.repositoryId },
1891
+ workId,
1892
+ receipt,
1893
+ requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'completed_changed',
1894
+ requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'repository_change',
1895
+ ),
1896
+ );
1897
+ if (!fencedCompletion.allowed) {
1898
+ throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fencedCompletion.reason}`);
1899
+ }
1900
+ }
1901
+ // Successful WorkContract completion always ends controller ownership.
1902
+ // Physical branch/worktree retention is represented by finalization stages
1903
+ // and completion-receipt warnings; it must not keep a mutation owner live.
1904
+ releasePreparedWorkOwnership(ctx, current);
1905
+ updateExecutionSession(ctx.controllerHome, identity, {
1906
+ activeWorkId: undefined,
1907
+ activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
1908
+ });
1909
+ }
1910
+ return { work: compactHandle(current), stages: current.finalization, completed: complete, ...(remoteDelivery ? { remoteDelivery } : {}), idempotent: !wants.commit && !wants.merge && !wants.cleanup && current.finalization.validation === 'done' };
1911
+ }
1912
+
1913
+ // WORK_FINALIZATION_SERVICE_END