@fitlab-ai/agent-infra 0.11.3 → 0.11.5

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 (172) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -6
  4. package/dist/lib/decide.js +4 -41
  5. package/dist/lib/git/branch-worktree.js +45 -0
  6. package/dist/lib/internal/cli-route-inventory.js +1 -4
  7. package/dist/lib/internal/task-activity.js +7 -40
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-event.js +4 -37
  10. package/dist/lib/internal/task-finalization.js +48 -1
  11. package/dist/lib/internal/task-ledger.js +14 -44
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +2 -4
  14. package/dist/lib/internal/task-review.js +5 -37
  15. package/dist/lib/internal/task-verify.js +3 -47
  16. package/dist/lib/internal/task-warning.js +4 -40
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/verification-sync.js +12 -49
  20. package/dist/lib/sandbox/control/client.js +4 -1
  21. package/dist/lib/sandbox/control/executor.js +12 -4
  22. package/dist/lib/sandbox/control/protocol.js +2 -1
  23. package/dist/lib/sandbox/control/server.js +26 -143
  24. package/dist/lib/sandbox/removal.js +15 -8
  25. package/dist/lib/sandbox/task-cleanup.js +13 -20
  26. package/dist/lib/task/activity-intent.js +1 -2
  27. package/dist/lib/task/artifact-command.js +11 -4
  28. package/dist/lib/task/artifact-lifecycle.js +217 -85
  29. package/dist/lib/task/artifact-operations.js +37 -28
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +8 -7
  32. package/dist/lib/task/artifact-selection.js +25 -0
  33. package/dist/lib/task/capabilities.js +158 -39
  34. package/dist/lib/task/commands/status.js +9 -2
  35. package/dist/lib/task/control-authority.js +12 -168
  36. package/dist/lib/task/decision-intents.js +14 -4
  37. package/dist/lib/task/delivery.js +1 -43
  38. package/dist/lib/task/events.js +68 -73
  39. package/dist/lib/task/finalization-handoff.js +92 -0
  40. package/dist/lib/task/finalization.js +423 -154
  41. package/dist/lib/task/intermediate-cleanup.js +9 -148
  42. package/dist/lib/task/ledger-intents.js +159 -20
  43. package/dist/lib/task/lifecycle-path.js +75 -0
  44. package/dist/lib/task/lifecycle.js +60 -11
  45. package/dist/lib/task/local-artifact-finalization.js +13 -47
  46. package/dist/lib/task/orchestration.js +17 -7
  47. package/dist/lib/task/review-command.js +1 -2
  48. package/dist/lib/task/review-finalization.js +4 -18
  49. package/dist/lib/task/rework-intent.js +70 -4
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +25 -30
  52. package/dist/lib/task/verification.js +2 -13
  53. package/dist/lib/task/workflow-warning-intents.js +23 -10
  54. package/dist/lib/task/workspace-snapshot.js +7 -2
  55. package/dist/lib/task/write.js +1 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/decide.ts +5 -35
  58. package/lib/git/branch-worktree.ts +41 -0
  59. package/lib/internal/cli-route-inventory.ts +1 -3
  60. package/lib/internal/task-activity.ts +6 -34
  61. package/lib/internal/task-artifact.ts +1 -1
  62. package/lib/internal/task-event.ts +4 -37
  63. package/lib/internal/task-finalization.ts +47 -1
  64. package/lib/internal/task-ledger.ts +15 -41
  65. package/lib/internal/task-lifecycle.ts +2 -2
  66. package/lib/internal/task-operation-registry.ts +2 -4
  67. package/lib/internal/task-review.ts +5 -31
  68. package/lib/internal/task-verify.ts +3 -39
  69. package/lib/internal/task-warning.ts +3 -32
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/pr-summary.ts +4 -25
  72. package/lib/platform/verification-sync.ts +13 -53
  73. package/lib/sandbox/control/client.ts +5 -1
  74. package/lib/sandbox/control/executor.ts +13 -4
  75. package/lib/sandbox/control/protocol.ts +3 -2
  76. package/lib/sandbox/control/server.ts +21 -146
  77. package/lib/sandbox/removal.ts +23 -7
  78. package/lib/sandbox/task-cleanup.ts +12 -20
  79. package/lib/task/activity-intent.ts +1 -2
  80. package/lib/task/artifact-command.ts +14 -4
  81. package/lib/task/artifact-lifecycle.ts +236 -85
  82. package/lib/task/artifact-operations.ts +59 -28
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +10 -7
  85. package/lib/task/artifact-selection.ts +53 -0
  86. package/lib/task/capabilities.ts +154 -40
  87. package/lib/task/commands/status.ts +11 -2
  88. package/lib/task/control-authority.ts +17 -189
  89. package/lib/task/decision-intents.ts +11 -4
  90. package/lib/task/delivery.ts +1 -40
  91. package/lib/task/events.ts +73 -74
  92. package/lib/task/finalization-handoff.ts +104 -0
  93. package/lib/task/finalization.ts +388 -122
  94. package/lib/task/intermediate-cleanup.ts +9 -125
  95. package/lib/task/ledger-intents.ts +157 -20
  96. package/lib/task/lifecycle-path.ts +98 -0
  97. package/lib/task/lifecycle.ts +58 -13
  98. package/lib/task/local-artifact-finalization.ts +14 -56
  99. package/lib/task/orchestration.ts +26 -8
  100. package/lib/task/review-command.ts +2 -5
  101. package/lib/task/review-finalization.ts +4 -20
  102. package/lib/task/rework-intent.ts +75 -5
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +29 -30
  105. package/lib/task/verification.ts +3 -16
  106. package/lib/task/workflow-warning-intents.ts +20 -10
  107. package/lib/task/workspace-snapshot.ts +3 -2
  108. package/lib/task/write.ts +1 -4
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/review-handshake.en.md +4 -4
  111. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  112. package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
  113. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
  114. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  115. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  116. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  117. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  118. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  119. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  120. package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
  121. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
  122. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  125. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  128. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  129. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  130. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  131. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  132. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  137. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  138. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  139. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  140. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  141. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  142. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  143. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  144. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  145. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  146. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  147. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  148. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  149. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  150. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  151. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  152. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  153. package/templates/.agents/templates/task.en.md +2 -2
  154. package/templates/.agents/templates/task.zh-CN.md +2 -2
  155. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  156. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  157. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  158. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  159. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  160. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  161. package/dist/lib/internal/task-override.js +0 -138
  162. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  163. package/dist/lib/task/artifact-recovery.js +0 -476
  164. package/dist/lib/task/artifact-repair-intent.js +0 -181
  165. package/dist/lib/task/guard-override.js +0 -31
  166. package/dist/lib/task/human-override.js +0 -1259
  167. package/lib/internal/task-override.ts +0 -144
  168. package/lib/sandbox/control/finalization-response.ts +0 -35
  169. package/lib/task/artifact-recovery.ts +0 -572
  170. package/lib/task/artifact-repair-intent.ts +0 -227
  171. package/lib/task/guard-override.ts +0 -50
  172. package/lib/task/human-override.ts +0 -1522
@@ -3,16 +3,19 @@ import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { syncPlatformComment } from '../platform/issue-comments.js';
5
5
  import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
6
- import { backfillCompletionComments } from '../platform/completion-backfill.js';
6
+ import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
7
7
  import { taskIssueIdentity } from '../platform/task-identities.js';
8
8
  import { parseTaskFrontmatter } from './frontmatter.js';
9
- import { applyTaskLifecycle } from './lifecycle.js';
9
+ import { applyTaskLifecycle, inspectTaskLifecycleProgress } from './lifecycle.js';
10
10
  import { resolveTaskRef } from './resolve-ref.js';
11
11
  import { inspectShortIdRegistry } from './short-id.js';
12
12
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
13
13
  import { verifyTaskEvent } from './verification.js';
14
14
  import { projectFinalizationWarning } from './workflow-warning-intents.js';
15
15
  import { mergeOperationWarnings } from './operation-outcome.js';
16
+ import { readTaskFinalizationHandoff } from './finalization-handoff.js';
17
+ import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
18
+ import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.js';
16
19
  const RECEIPT_VERSION = 4;
17
20
  const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
18
21
  function finalizationRoot(repoRoot) {
@@ -89,19 +92,32 @@ function validateReceipt(value, taskId) {
89
92
  const receipt = value;
90
93
  const controlBinding = receipt.controlBinding;
91
94
  const states = ['pending', 'done', 'skipped'];
92
- if (receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
95
+ const requiredKeys = [
96
+ 'version', 'taskId', 'intent', 'receiptId', 'revision', 'lifecycle', 'taskComment',
97
+ 'verification', 'summary', 'warningProjection', 'warnings', 'updatedAt', 'lastError'
98
+ ];
99
+ if (receipt.controlBinding !== undefined)
100
+ requiredKeys.push('controlBinding');
101
+ if (Object.keys(receipt).sort().join('\0') !== requiredKeys.sort().join('\0')
102
+ ||
103
+ receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
93
104
  || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
94
105
  || !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
95
- || !states.includes(String(receipt.lifecycle))
96
- || !states.includes(String(receipt.taskComment))
97
- || !states.includes(String(receipt.verification))
98
- || !states.includes(String(receipt.summary))
99
- || !['pending', 'done'].includes(String(receipt.warningProjection))
106
+ || !states.includes(receipt.lifecycle)
107
+ || !states.includes(receipt.taskComment)
108
+ || !states.includes(receipt.verification)
109
+ || !states.includes(receipt.summary)
110
+ || !['pending', 'done'].includes(receipt.warningProjection)
100
111
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
101
- || typeof receipt.updatedAt !== 'string'
102
- || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
112
+ || typeof receipt.updatedAt !== 'string' || receipt.updatedAt.length === 0
113
+ || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
114
+ || Object.keys(receipt.lastError).sort().join('\0') !== ['code', 'message', 'retryable'].join('\0')
115
+ || typeof receipt.lastError.code !== 'string'
116
+ || typeof receipt.lastError.message !== 'string'
117
+ || typeof receipt.lastError.retryable !== 'boolean'))
103
118
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
104
119
  || Array.isArray(controlBinding)
120
+ || Object.keys(controlBinding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
105
121
  || typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
106
122
  || typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId))))
107
123
  throw new Error('receipt schema is invalid');
@@ -134,6 +150,12 @@ function writeReceipt(repoRoot, receipt) {
134
150
  throw error;
135
151
  }
136
152
  }
153
+ export function bindTaskFinalizationReceipt(repoRoot, taskId, controlBinding) {
154
+ const receipt = readReceipt(path.resolve(repoRoot), taskId);
155
+ if (!receipt)
156
+ throw new Error('TASK_FINALIZATION_PREPARATION_REQUIRED');
157
+ return updateReceipt(path.resolve(repoRoot), receipt, { controlBinding, lastError: null });
158
+ }
137
159
  function updateReceipt(repoRoot, receipt, patch) {
138
160
  const current = readReceipt(repoRoot, receipt.taskId);
139
161
  if (!current || current.receiptId !== receipt.receiptId)
@@ -221,6 +243,77 @@ function resolveStepWarnings(receipt, step) {
221
243
  ? { ...warning, status: 'resolved', resolvedAt: now() }
222
244
  : warning);
223
245
  }
246
+ function verificationWarnings(result) {
247
+ const warnings = [];
248
+ for (const invocation of result.invocations) {
249
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
250
+ for (const value of checks) {
251
+ if (!value || typeof value !== 'object' || Array.isArray(value))
252
+ continue;
253
+ const check = value;
254
+ const status = check.effectiveStatus ?? check.status;
255
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
256
+ if (!target || status === 'pass')
257
+ continue;
258
+ const blocked = status === 'blocked';
259
+ const message = [check.reason, check.message, check.action]
260
+ .filter((item) => typeof item === 'string' && item.length > 0)
261
+ .join(' - ');
262
+ warnings.push({
263
+ step: 'verification', target, code: blocked ? 'CHECK_BLOCKED' : 'CHECK_FAILED',
264
+ retryable: blocked, severity: 'ACTION_REQUIRED',
265
+ message: message || `verification ${String(status)}`
266
+ });
267
+ }
268
+ }
269
+ return mergeOperationWarnings(warnings);
270
+ }
271
+ function applyVerificationWarnings(receipt, result) {
272
+ if (result.status === 'pass')
273
+ return resolveStepWarnings(receipt, 'verification');
274
+ const observed = new Map();
275
+ for (const invocation of result.invocations) {
276
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
277
+ for (const value of checks) {
278
+ if (!value || typeof value !== 'object' || Array.isArray(value))
279
+ continue;
280
+ const check = value;
281
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
282
+ if (!target)
283
+ continue;
284
+ const status = check.effectiveStatus ?? check.status;
285
+ if (status === 'pass')
286
+ observed.set(target, 'pass');
287
+ }
288
+ }
289
+ for (const warning of verificationWarnings(result))
290
+ observed.set(warning.target, warning);
291
+ if (observed.size === 0) {
292
+ return replaceWarning(receipt, warningFromError('verification', verificationFailure(result)), 'open');
293
+ }
294
+ let warnings = [...receipt.warnings];
295
+ for (const [target, current] of observed) {
296
+ if (current === 'pass') {
297
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target && warning.status === 'open'
298
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
299
+ : warning);
300
+ continue;
301
+ }
302
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target
303
+ && warning.status === 'open' && warning.code !== current.code
304
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
305
+ : warning);
306
+ const key = warningKey(current);
307
+ warnings = [...warnings.filter((warning) => warningKey(warning) !== key), warningRecord(current, 'open')];
308
+ }
309
+ return warnings;
310
+ }
311
+ function shouldRunBackfill(receipt) {
312
+ return receipt.lifecycle !== 'done'
313
+ || receipt.taskComment === 'pending'
314
+ || receipt.verification === 'pending'
315
+ || receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open');
316
+ }
224
317
  function issueCapability(receipt, scope) {
225
318
  return {
226
319
  receiptId: receipt.receiptId,
@@ -236,6 +329,21 @@ function capabilityError(code, message) {
236
329
  Object.assign(error, { code });
237
330
  return error;
238
331
  }
332
+ function hasPreparedTaskDocument(repoRoot, taskId) {
333
+ const resolved = resolveTaskRef(taskId, { repoRoot });
334
+ if (!resolved.ok)
335
+ return false;
336
+ if (resolved.state === 'completed')
337
+ return true;
338
+ if (resolved.state !== 'active')
339
+ return false;
340
+ try {
341
+ return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed';
342
+ }
343
+ catch {
344
+ return false;
345
+ }
346
+ }
239
347
  function mutationKeys(value) {
240
348
  return Object.keys(value).sort();
241
349
  }
@@ -305,9 +413,8 @@ function mutationPatch(current, mutation) {
305
413
  : { warningProjection: 'pending', lastError: mutation.error };
306
414
  }
307
415
  function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, consumed) {
308
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
309
- if (!resolved.ok || resolved.state !== 'completed') {
310
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
416
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
417
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
311
418
  }
312
419
  const current = readReceipt(repoRoot, receipt.taskId);
313
420
  if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
@@ -319,11 +426,10 @@ function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability
319
426
  return next;
320
427
  }
321
428
  function applyFinalizationReceiptMutation(repoRoot, receipt, capability, mutation) {
322
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
323
- if (!resolved.ok || resolved.state !== 'completed') {
324
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
429
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
430
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
325
431
  }
326
- return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
432
+ return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
327
433
  }
328
434
  function warningKey(warning) {
329
435
  return `${warning.step}\0${warning.code}\0${warning.target}`;
@@ -334,7 +440,8 @@ function reconcileWarningProjection(repoRoot, taskId, receipt, consumed) {
334
440
  const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
335
441
  for (const warning of warnings) {
336
442
  try {
337
- const projected = projectFinalizationWarning(taskId, warning, { repoRoot });
443
+ const superseded = warning.status === 'resolved' && receipt.warnings.some((item) => item.status === 'open' && item.step === warning.step && item.target === warning.target && item.code !== warning.code);
444
+ const projected = projectFinalizationWarning(taskId, warning, { repoRoot, resolution: superseded ? 'superseded' : 'passed' });
338
445
  if (projected.status === 'failed') {
339
446
  const detail = {
340
447
  code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
@@ -433,8 +540,12 @@ async function syncPendingSummary(input) {
433
540
  let { receipt } = input;
434
541
  try {
435
542
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
436
- if (!resolved.ok || resolved.state !== 'completed')
437
- throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
543
+ if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId))
544
+ throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
545
+ if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
546
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
547
+ return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
548
+ }
438
549
  if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
439
550
  receipt = updateReceipt(input.repoRoot, receipt, {
440
551
  summary: 'skipped', lastError: null
@@ -509,7 +620,19 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
509
620
  error: hardError ? error : postLifecyclePending ? null : error
510
621
  };
511
622
  }
512
- async function applyUnderLock(request, taskId, options) {
623
+ function preparedResult(taskId, receipt, steps, changed) {
624
+ return {
625
+ status: 'prepared', changed, taskId,
626
+ backfill: steps.backfill ?? null,
627
+ lifecycle: steps.lifecycle ?? null,
628
+ taskComment: steps.taskComment ?? null,
629
+ verification: steps.verification ?? null,
630
+ summary: steps.summary ?? null,
631
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt),
632
+ result: 'prepared', warnings: openWarnings(receipt), error: null
633
+ };
634
+ }
635
+ async function prepareUnderLock(request, taskId, options) {
513
636
  const repoRoot = path.resolve(options.repoRoot);
514
637
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
515
638
  const backfill = options.backfill ?? backfillCompletionComments;
@@ -528,8 +651,15 @@ async function applyUnderLock(request, taskId, options) {
528
651
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
529
652
  }
530
653
  if (options.controlBinding && (receipt.controlBinding?.generation !== options.controlBinding.generation
531
- || receipt.controlBinding.requestId !== options.controlBinding.requestId))
532
- receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
654
+ || receipt.controlBinding.requestId !== options.controlBinding.requestId)) {
655
+ const state = resolveTaskRef(taskId, { repoRoot });
656
+ if (!(receipt.lifecycle === 'done' && state.ok && state.state === 'completed')) {
657
+ return failed(taskId, {
658
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
659
+ message: 'canonical receipt is not bound to the current sandbox request', retryable: false
660
+ });
661
+ }
662
+ }
533
663
  const preflightState = resolveTaskRef(taskId, { repoRoot });
534
664
  if (options.preflight && preflightState.ok && preflightState.state === 'active') {
535
665
  try {
@@ -551,80 +681,87 @@ async function applyUnderLock(request, taskId, options) {
551
681
  }
552
682
  let changed = false;
553
683
  let backfillResult = null;
554
- try {
555
- const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
556
- backfillResult = commentStep(result);
557
- const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
558
- if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
559
- const detail = backfillResult.error ?? {
560
- code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
561
- };
684
+ let runBackfill = shouldRunBackfill(receipt);
685
+ if (!runBackfill && receipt.lifecycle === 'done') {
686
+ const eligibility = inspectCompletionBackfillEligibility(taskId, { cwd: repoRoot });
687
+ if (eligibility.status === 'failed')
688
+ return failed(taskId, eligibility.error, {
689
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
690
+ });
691
+ runBackfill = eligibility.eligible;
692
+ }
693
+ if (receipt.lifecycle === 'done' && runBackfill && receipt.summary === 'done') {
694
+ receipt = updateReceipt(repoRoot, receipt, { summary: 'pending' });
695
+ }
696
+ if (runBackfill)
697
+ try {
698
+ const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
699
+ backfillResult = commentStep(result);
700
+ const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
701
+ if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
702
+ const detail = backfillResult.error ?? {
703
+ code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
704
+ };
705
+ if (receipt.lifecycle !== 'done')
706
+ return failed(taskId, detail, {
707
+ backfill: backfillResult,
708
+ completedSteps: completedSteps(receipt),
709
+ pendingSteps: pendingSteps(receipt)
710
+ });
711
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
712
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
713
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
714
+ if (!detail.retryable)
715
+ return failed(taskId, detail, {
716
+ changed, backfill: backfillResult, completedSteps: completedSteps(receipt),
717
+ pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
718
+ });
719
+ return terminalResult(taskId, receipt, { backfill: backfillResult }, changed, detail);
720
+ }
721
+ changed = result.changed;
722
+ if (receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open')) {
723
+ const warnings = receipt.warnings.map((warning) => warning.step === 'backfill' && warning.status === 'open'
724
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
725
+ : warning);
726
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: null });
727
+ }
728
+ if (result.changed && receipt.taskComment !== 'pending') {
729
+ receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
730
+ }
731
+ }
732
+ catch (error) {
733
+ const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
734
+ if (receipt.lifecycle === 'done') {
735
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
736
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
737
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
738
+ return terminalResult(taskId, receipt, {
739
+ backfill: { status: 'blocked', changed: false, error: detail }
740
+ }, changed, detail);
741
+ }
562
742
  return failed(taskId, detail, {
563
- backfill: backfillResult,
743
+ backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
564
744
  completedSteps: completedSteps(receipt),
565
745
  pendingSteps: pendingSteps(receipt)
566
746
  });
567
747
  }
568
- changed = result.changed;
569
- if (result.changed && receipt.taskComment !== 'pending') {
570
- receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
571
- }
572
- }
573
- catch (error) {
574
- const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
575
- return failed(taskId, detail, {
576
- backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
577
- completedSteps: completedSteps(receipt),
578
- pendingSteps: pendingSteps(receipt)
579
- });
580
- }
748
+ else
749
+ backfillResult = { status: 'no-op', changed: false, error: null };
581
750
  let lifecycleResult = null;
582
751
  try {
583
752
  const resolved = resolveTaskRef(taskId, { repoRoot });
584
- const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
585
- if (registry && registry.status !== 'valid') {
586
- const detail = {
587
- code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
588
- message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
589
- retryable: false
590
- };
591
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
592
- return terminalResult(taskId, receipt, {
593
- backfill: backfillResult,
594
- lifecycle: { status: 'failed', changed: false, error: detail },
595
- taskComment: null,
596
- verification: null
597
- }, changed, detail);
598
- }
599
- const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
600
- if (shortIds?.has(taskId)) {
601
- const detail = {
602
- code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
603
- message: 'completed task still has an active short-id registry entry',
604
- retryable: false
605
- };
606
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
607
- return terminalResult(taskId, receipt, {
608
- backfill: backfillResult,
609
- lifecycle: { status: 'failed', changed: false, error: detail },
610
- taskComment: null,
611
- verification: null
612
- }, changed, detail);
613
- }
614
- const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
615
- if (lifecycleDone) {
753
+ if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
616
754
  lifecycleResult = { status: 'no-op', changed: false, error: null };
617
755
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
618
756
  }
619
757
  else {
620
- const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
758
+ const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
621
759
  lifecycleResult = lifecycleStep(result);
622
760
  if (result.status !== 'applied' && result.status !== 'no-op') {
623
761
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
624
762
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
625
763
  }
626
764
  changed = changed || result.changed;
627
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
628
765
  }
629
766
  }
630
767
  catch (error) {
@@ -637,27 +774,26 @@ async function applyUnderLock(request, taskId, options) {
637
774
  }
638
775
  let taskComment = null;
639
776
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
640
- const initialComment = await syncPendingTaskComment({
641
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
642
- });
643
- receipt = initialComment.receipt;
644
- taskComment = initialComment.step;
645
- changed = changed || initialComment.changed;
646
- if (initialComment.error) {
647
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
648
- }
649
777
  let verification = null;
650
- try {
651
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
652
- const verificationPending = receipt.verification === 'pending';
653
- const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
654
- verification = verificationStep(result);
655
- if (result.status === 'pass') {
656
- if (verificationPending) {
657
- const capability = issueCapability(receipt, 'verification');
658
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
659
- scope: 'verification', operation: 'succeeded'
660
- }, consumedCapabilities);
778
+ if (receipt.verification !== 'pending') {
779
+ verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
780
+ }
781
+ else {
782
+ try {
783
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
784
+ const verificationState = resolveTaskRef(taskId, { repoRoot });
785
+ const verificationEvent = verificationState.ok && verificationState.state === 'completed'
786
+ ? 'complete-task.completed'
787
+ : 'complete-task.prepared';
788
+ const result = await verify({ taskRef: taskId, event: verificationEvent }, { repoRoot });
789
+ verification = verificationStep(result);
790
+ if (result.status === 'pass') {
791
+ const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
792
+ const warnings = applyVerificationWarnings(receipt, result);
793
+ receipt = updateReceipt(repoRoot, receipt, {
794
+ verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
795
+ warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
796
+ });
661
797
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
662
798
  const finalComment = await syncPendingTaskComment({
663
799
  repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
@@ -671,72 +807,58 @@ async function applyUnderLock(request, taskId, options) {
671
807
  }
672
808
  }
673
809
  else {
674
- receipt = updateReceipt(repoRoot, receipt, { lastError: null });
675
- }
676
- }
677
- else {
678
- const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
679
- if (verificationPending) {
680
- const capability = issueCapability(receipt, 'verification');
681
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
682
- scope: 'verification', operation: 'failed', error: detail
683
- }, consumedCapabilities);
684
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
685
- }
686
- else {
687
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
810
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
811
+ const warnings = applyVerificationWarnings(receipt, result);
688
812
  receipt = updateReceipt(repoRoot, receipt, {
689
813
  verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
690
814
  });
691
815
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
692
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
816
+ const warningComment = await syncPendingTaskComment({
817
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
818
+ });
693
819
  receipt = warningComment.receipt;
694
- if (warningComment.step.status !== 'no-op')
695
- taskComment = warningComment.step;
820
+ taskComment = warningComment.step;
696
821
  changed = changed || warningComment.changed;
822
+ if (warningComment.error) {
823
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
824
+ }
825
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
697
826
  }
698
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
699
827
  }
700
- }
701
- catch (error) {
702
- const detail = errorOf(error, 'VERIFY_FAILED', true);
703
- try {
704
- if (receipt.verification === 'pending') {
705
- const capability = issueCapability(receipt, 'verification');
706
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
707
- scope: 'verification', operation: 'failed', error: detail
708
- }, consumedCapabilities);
709
- }
710
- else {
828
+ catch (error) {
829
+ const detail = errorOf(error, 'VERIFY_FAILED', true);
830
+ try {
711
831
  const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
712
832
  receipt = updateReceipt(repoRoot, receipt, {
713
833
  verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
714
834
  });
835
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
715
836
  }
716
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
717
- }
718
- catch (writeError) {
719
- const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
720
- const hardError = {
721
- code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
722
- message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
723
- retryable: true
724
- };
725
- return terminalResult(taskId, receipt, {
726
- backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
727
- }, changed, hardError);
728
- }
729
- try {
730
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
731
- receipt = warningComment.receipt;
732
- if (warningComment.step.status !== 'no-op')
733
- taskComment = warningComment.step;
734
- changed = changed || warningComment.changed;
837
+ catch (writeError) {
838
+ const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
839
+ const hardError = {
840
+ code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
841
+ message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
842
+ retryable: true
843
+ };
844
+ return terminalResult(taskId, receipt, {
845
+ backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
846
+ }, changed, hardError);
847
+ }
848
+ try {
849
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
850
+ receipt = warningComment.receipt;
851
+ if (warningComment.step.status !== 'no-op')
852
+ taskComment = warningComment.step;
853
+ changed = changed || warningComment.changed;
854
+ }
855
+ catch { /* preserve the primary error */ }
856
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
735
857
  }
736
- catch { /* preserve the primary error */ }
737
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
738
858
  }
739
- let summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
859
+ let summary = receipt.summary === 'pending'
860
+ ? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
861
+ : { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
740
862
  receipt = summary.receipt;
741
863
  changed = changed || summary.changed;
742
864
  if (summary.error) {
@@ -744,9 +866,20 @@ async function applyUnderLock(request, taskId, options) {
744
866
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
745
867
  }
746
868
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
747
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
869
+ if (receipt.taskComment === 'pending') {
870
+ const finalComment = await syncPendingTaskComment({
871
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
872
+ });
873
+ receipt = finalComment.receipt;
874
+ taskComment = finalComment.step;
875
+ changed = changed || finalComment.changed;
876
+ if (finalComment.error) {
877
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, finalComment.error);
878
+ }
879
+ }
880
+ return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
748
881
  }
749
- async function applyTaskFinalization(request, options) {
882
+ async function prepareTaskFinalization(request, options) {
750
883
  if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
751
884
  return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
752
885
  }
@@ -755,7 +888,7 @@ async function applyTaskFinalization(request, options) {
755
888
  if (!resolved.ok)
756
889
  return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
757
890
  try {
758
- return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
891
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
759
892
  }
760
893
  catch (error) {
761
894
  const detail = error instanceof TaskExecutionLockError
@@ -764,5 +897,141 @@ async function applyTaskFinalization(request, options) {
764
897
  return failed(resolved.taskId, detail);
765
898
  }
766
899
  }
767
- export { applyFinalizationReceiptMutation, applyTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
900
+ async function commitPreparedTaskFinalization(request, options) {
901
+ if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
902
+ return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
903
+ }
904
+ const repoRoot = path.resolve(options.repoRoot);
905
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot });
906
+ if (!resolved.ok)
907
+ return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
908
+ try {
909
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
910
+ if (options.controlBinding && options.manifestPath) {
911
+ try {
912
+ const manifest = readSandboxControlManifest(options.manifestPath);
913
+ const identity = validateSandboxControlIdentity({
914
+ publicStatusDir: manifest.publicStatusDir,
915
+ root: path.dirname(path.resolve(options.manifestPath)),
916
+ mode: manifest.mode,
917
+ taskId: manifest.taskId,
918
+ generation: manifest.generation,
919
+ controlRootId: manifest.controlRootId
920
+ });
921
+ if (identity.state !== 'valid' || manifest.taskId !== resolved.taskId
922
+ || manifest.generation !== options.controlBinding.generation
923
+ || fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
924
+ throw new Error('current sandbox manifest does not match finalization request');
925
+ }
926
+ }
927
+ catch (error) {
928
+ return failed(resolved.taskId, {
929
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
930
+ message: error instanceof Error ? error.message : String(error), retryable: false
931
+ });
932
+ }
933
+ }
934
+ let receipt = readReceipt(repoRoot, resolved.taskId);
935
+ if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
936
+ try {
937
+ const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
938
+ receipt = validateReceipt(handoff.receipt, resolved.taskId);
939
+ if (receipt.lifecycle !== 'pending')
940
+ throw new Error('handoff lifecycle must be pending');
941
+ if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
942
+ throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
943
+ }
944
+ writeReceipt(repoRoot, receipt);
945
+ try {
946
+ handoff.cleanup();
947
+ }
948
+ catch { /* keep the persisted canonical receipt usable */ }
949
+ }
950
+ catch (error) {
951
+ return failed(resolved.taskId, {
952
+ code: 'TASK_FINALIZATION_HANDOFF_INVALID',
953
+ message: error instanceof Error ? error.message : String(error), retryable: false
954
+ });
955
+ }
956
+ }
957
+ if (!receipt)
958
+ return failed(resolved.taskId, {
959
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
960
+ });
961
+ if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
962
+ return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
963
+ }
964
+ if (options.controlBinding) {
965
+ const proof = inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent);
966
+ const oldBinding = receipt.controlBinding;
967
+ if (!oldBinding || oldBinding.generation !== options.controlBinding.generation || proof === 'unknown') {
968
+ return failed(resolved.taskId, {
969
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
970
+ message: 'canonical receipt has no valid host recovery proof', retryable: false
971
+ });
972
+ }
973
+ if (oldBinding.requestId !== options.controlBinding.requestId && proof === 'not-started') {
974
+ if (receipt.lifecycle !== 'pending' || !options.handoffDirectory || !request.handoffSha256) {
975
+ return failed(resolved.taskId, {
976
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
977
+ message: 'pending receipt cannot be rebound without a current handoff', retryable: false
978
+ });
979
+ }
980
+ try {
981
+ const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
982
+ const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
983
+ if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
984
+ || nextReceipt.lifecycle !== 'pending')
985
+ throw new Error('handoff is not continuous with canonical receipt');
986
+ receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
987
+ try {
988
+ handoff.cleanup();
989
+ }
990
+ catch { /* keep the persisted canonical receipt usable */ }
991
+ }
992
+ catch (error) {
993
+ return failed(resolved.taskId, {
994
+ code: 'TASK_FINALIZATION_HANDOFF_INVALID',
995
+ message: error instanceof Error ? error.message : String(error), retryable: false
996
+ });
997
+ }
998
+ }
999
+ }
1000
+ if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1001
+ return failed(resolved.taskId, {
1002
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
1003
+ }, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
1004
+ }
1005
+ const registry = inspectShortIdRegistry(repoRoot);
1006
+ if (registry.status !== 'valid')
1007
+ return failed(resolved.taskId, {
1008
+ code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
1009
+ message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
1010
+ retryable: false
1011
+ });
1012
+ const lifecycle = options.lifecycle ?? applyTaskLifecycle;
1013
+ const result = lifecycle({ taskRef: resolved.taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
1014
+ const step = lifecycleStep(result);
1015
+ if (result.status !== 'applied' && result.status !== 'no-op') {
1016
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
1017
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
1018
+ }
1019
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
1020
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
1021
+ });
1022
+ }
1023
+ catch (error) {
1024
+ const detail = error instanceof TaskExecutionLockError
1025
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
1026
+ : errorOf(error, 'TASK_FINALIZATION_FAILED');
1027
+ return failed(resolved.taskId, detail);
1028
+ }
1029
+ }
1030
+ async function applyTaskFinalization(request, options) {
1031
+ const prepared = await prepareTaskFinalization(request, options);
1032
+ if (prepared.status !== 'prepared')
1033
+ return prepared;
1034
+ return commitPreparedTaskFinalization(request, options);
1035
+ }
1036
+ export { applyFinalizationReceiptMutation, commitPreparedTaskFinalization, applyTaskFinalization, prepareTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
768
1037
  //# sourceMappingURL=finalization.js.map