@fitlab-ai/agent-infra 0.11.2 → 0.11.4

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 (162) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +7 -2
  4. package/dist/lib/agent-clients/adapter.js +3 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  8. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  9. package/dist/lib/internal/cli-route-inventory.js +1 -1
  10. package/dist/lib/internal/manual-validation.js +22 -27
  11. package/dist/lib/internal/sandbox-control.js +1 -43
  12. package/dist/lib/internal/task-artifact.js +1 -1
  13. package/dist/lib/internal/task-finalization.js +41 -1
  14. package/dist/lib/internal/task-ledger.js +13 -5
  15. package/dist/lib/internal/task-lifecycle.js +1 -1
  16. package/dist/lib/internal/task-operation-registry.js +7 -2
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/issue-comments.js +2 -31
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/sandbox/control/client.js +3 -44
  21. package/dist/lib/sandbox/control/executor.js +14 -79
  22. package/dist/lib/sandbox/control/protocol.js +5 -64
  23. package/dist/lib/sandbox/control/server.js +49 -343
  24. package/dist/lib/sandbox/control/state.js +1 -7
  25. package/dist/lib/sandbox/control/task-view.js +1 -2
  26. package/dist/lib/sandbox/task-cleanup.js +13 -20
  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 +1 -26
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +1 -0
  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 +37 -38
  36. package/dist/lib/task/control-recovery.js +3 -42
  37. package/dist/lib/task/decision-intents.js +12 -0
  38. package/dist/lib/task/delegation-receipts.js +2 -2
  39. package/dist/lib/task/delivery.js +1 -43
  40. package/dist/lib/task/events.js +80 -64
  41. package/dist/lib/task/finalization-artifacts.js +1 -3
  42. package/dist/lib/task/finalization-state.js +1 -1
  43. package/dist/lib/task/finalization.js +319 -234
  44. package/dist/lib/task/ledger-intents.js +157 -15
  45. package/dist/lib/task/lifecycle-path.js +75 -0
  46. package/dist/lib/task/lifecycle.js +11 -1
  47. package/dist/lib/task/local-artifact-finalization.js +1 -20
  48. package/dist/lib/task/manual-validation-receipt.js +8 -3
  49. package/dist/lib/task/orchestration.js +29 -13
  50. package/dist/lib/task/review-finalization.js +3 -16
  51. package/dist/lib/task/rework-intent.js +70 -4
  52. package/dist/lib/task/verification-engine.js +20 -30
  53. package/dist/lib/task/verification.js +1 -0
  54. package/dist/lib/task/workflow-warning-intents.js +21 -6
  55. package/dist/lib/task/workspace-snapshot.js +7 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/agent-clients/adapter.ts +46 -0
  58. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  60. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  61. package/lib/agent-clients/adapters/codex.ts +10 -0
  62. package/lib/internal/cli-route-inventory.ts +1 -1
  63. package/lib/internal/manual-validation.ts +22 -26
  64. package/lib/internal/sandbox-control.ts +0 -42
  65. package/lib/internal/task-artifact.ts +1 -1
  66. package/lib/internal/task-finalization.ts +40 -1
  67. package/lib/internal/task-ledger.ts +12 -5
  68. package/lib/internal/task-lifecycle.ts +2 -2
  69. package/lib/internal/task-operation-registry.ts +8 -4
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/issue-comments.ts +1 -35
  72. package/lib/platform/pr-summary.ts +10 -2
  73. package/lib/sandbox/control/client.ts +5 -68
  74. package/lib/sandbox/control/executor.ts +14 -81
  75. package/lib/sandbox/control/protocol.ts +7 -83
  76. package/lib/sandbox/control/server.ts +47 -362
  77. package/lib/sandbox/control/state.ts +1 -9
  78. package/lib/sandbox/control/task-view.ts +1 -2
  79. package/lib/sandbox/task-cleanup.ts +12 -20
  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 +1 -26
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +1 -0
  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 +41 -48
  89. package/lib/task/control-recovery.ts +3 -43
  90. package/lib/task/decision-intents.ts +9 -0
  91. package/lib/task/delegation-receipts.ts +2 -2
  92. package/lib/task/delivery.ts +1 -40
  93. package/lib/task/events.ts +82 -64
  94. package/lib/task/finalization-artifacts.ts +1 -3
  95. package/lib/task/finalization-state.ts +1 -1
  96. package/lib/task/finalization.ts +310 -216
  97. package/lib/task/ledger-intents.ts +155 -15
  98. package/lib/task/lifecycle-path.ts +98 -0
  99. package/lib/task/lifecycle.ts +15 -2
  100. package/lib/task/local-artifact-finalization.ts +2 -25
  101. package/lib/task/manual-validation-receipt.ts +8 -2
  102. package/lib/task/orchestration.ts +56 -27
  103. package/lib/task/review-finalization.ts +3 -16
  104. package/lib/task/rework-intent.ts +75 -5
  105. package/lib/task/verification-engine.ts +19 -30
  106. package/lib/task/verification.ts +2 -1
  107. package/lib/task/workflow-warning-intents.ts +18 -6
  108. package/lib/task/workspace-snapshot.ts +3 -2
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
  111. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
  112. package/templates/.agents/rules/review-handshake.en.md +4 -4
  113. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  114. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  115. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  116. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  117. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  118. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  119. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  120. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  121. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  122. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  123. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  124. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  125. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  128. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  129. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  130. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  131. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  132. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  135. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  136. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  137. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  138. package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
  139. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
  140. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  141. package/templates/.agents/templates/task.en.md +2 -2
  142. package/templates/.agents/templates/task.zh-CN.md +2 -2
  143. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  144. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  145. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  146. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  147. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  148. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  149. package/dist/lib/sandbox/control/finalization-response.js +0 -35
  150. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  151. package/dist/lib/sandbox/control/workflow-executor.js +0 -142
  152. package/dist/lib/task/lifecycle-recovery.js +0 -549
  153. package/dist/lib/task/recovery-warning.js +0 -19
  154. package/dist/lib/task/workflow-command.js +0 -21
  155. package/dist/lib/task/workflow-dispatch.js +0 -213
  156. package/lib/sandbox/control/finalization-response.ts +0 -37
  157. package/lib/sandbox/control/task-workflow.ts +0 -95
  158. package/lib/sandbox/control/workflow-executor.ts +0 -156
  159. package/lib/task/lifecycle-recovery.ts +0 -691
  160. package/lib/task/recovery-warning.ts +0 -21
  161. package/lib/task/workflow-command.ts +0 -22
  162. package/lib/task/workflow-dispatch.ts +0 -214
@@ -3,6 +3,7 @@ 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, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
6
7
  import { taskIssueIdentity } from '../platform/task-identities.js';
7
8
  import { parseTaskFrontmatter } from './frontmatter.js';
8
9
  import { applyTaskLifecycle } from './lifecycle.js';
@@ -12,8 +13,8 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
12
13
  import { verifyTaskEvent } from './verification.js';
13
14
  import { projectFinalizationWarning } from './workflow-warning-intents.js';
14
15
  import { mergeOperationWarnings } from './operation-outcome.js';
15
- const RECEIPT_VERSION = 3;
16
- const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary', 'post-summary-verification'];
16
+ const RECEIPT_VERSION = 4;
17
+ const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
17
18
  function finalizationRoot(repoRoot) {
18
19
  return path.join(repoRoot, '.agents', 'workspace', '.task-finalization');
19
20
  }
@@ -34,11 +35,11 @@ function failed(taskId, error, overrides = {}) {
34
35
  status: error.retryable ? 'blocked' : 'failed',
35
36
  changed: false,
36
37
  taskId,
38
+ backfill: null,
37
39
  lifecycle: null,
38
40
  taskComment: null,
39
41
  verification: null,
40
42
  summary: null,
41
- postSummaryVerification: null,
42
43
  completedSteps: [],
43
44
  pendingSteps: [...FINALIZATION_STEPS],
44
45
  result: error.retryable ? 'blocked' : 'failed',
@@ -58,10 +59,8 @@ function emptyReceipt(taskId, controlBinding) {
58
59
  taskComment: 'pending',
59
60
  verification: 'pending',
60
61
  summary: 'pending',
61
- postSummaryVerification: 'pending',
62
62
  warningProjection: 'done',
63
63
  warnings: [],
64
- summarySha256: null,
65
64
  ...(controlBinding ? { controlBinding } : {}),
66
65
  updatedAt: now(),
67
66
  lastError: null
@@ -97,10 +96,8 @@ function validateReceipt(value, taskId) {
97
96
  || !states.includes(String(receipt.taskComment))
98
97
  || !states.includes(String(receipt.verification))
99
98
  || !states.includes(String(receipt.summary))
100
- || !states.includes(String(receipt.postSummaryVerification))
101
99
  || !['pending', 'done'].includes(String(receipt.warningProjection))
102
100
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
103
- || (receipt.summarySha256 !== null && (typeof receipt.summarySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(receipt.summarySha256)))
104
101
  || typeof receipt.updatedAt !== 'string'
105
102
  || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
106
103
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
@@ -154,8 +151,6 @@ function pendingSteps(receipt) {
154
151
  function stepState(receipt, step) {
155
152
  if (step === 'task-comment')
156
153
  return receipt.taskComment;
157
- if (step === 'post-summary-verification')
158
- return receipt.postSummaryVerification;
159
154
  return receipt[step];
160
155
  }
161
156
  function lifecycleStep(result) {
@@ -226,6 +221,77 @@ function resolveStepWarnings(receipt, step) {
226
221
  ? { ...warning, status: 'resolved', resolvedAt: now() }
227
222
  : warning);
228
223
  }
224
+ function verificationWarnings(result) {
225
+ const warnings = [];
226
+ for (const invocation of result.invocations) {
227
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
228
+ for (const value of checks) {
229
+ if (!value || typeof value !== 'object' || Array.isArray(value))
230
+ continue;
231
+ const check = value;
232
+ const status = check.effectiveStatus ?? check.status;
233
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
234
+ if (!target || status === 'pass')
235
+ continue;
236
+ const blocked = status === 'blocked';
237
+ const message = [check.reason, check.message, check.action]
238
+ .filter((item) => typeof item === 'string' && item.length > 0)
239
+ .join(' - ');
240
+ warnings.push({
241
+ step: 'verification', target, code: blocked ? 'CHECK_BLOCKED' : 'CHECK_FAILED',
242
+ retryable: blocked, severity: 'ACTION_REQUIRED',
243
+ message: message || `verification ${String(status)}`
244
+ });
245
+ }
246
+ }
247
+ return mergeOperationWarnings(warnings);
248
+ }
249
+ function applyVerificationWarnings(receipt, result) {
250
+ if (result.status === 'pass')
251
+ return resolveStepWarnings(receipt, 'verification');
252
+ const observed = new Map();
253
+ for (const invocation of result.invocations) {
254
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
255
+ for (const value of checks) {
256
+ if (!value || typeof value !== 'object' || Array.isArray(value))
257
+ continue;
258
+ const check = value;
259
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
260
+ if (!target)
261
+ continue;
262
+ const status = check.effectiveStatus ?? check.status;
263
+ if (status === 'pass')
264
+ observed.set(target, 'pass');
265
+ }
266
+ }
267
+ for (const warning of verificationWarnings(result))
268
+ observed.set(warning.target, warning);
269
+ if (observed.size === 0) {
270
+ return replaceWarning(receipt, warningFromError('verification', verificationFailure(result)), 'open');
271
+ }
272
+ let warnings = [...receipt.warnings];
273
+ for (const [target, current] of observed) {
274
+ if (current === 'pass') {
275
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target && warning.status === 'open'
276
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
277
+ : warning);
278
+ continue;
279
+ }
280
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target
281
+ && warning.status === 'open' && warning.code !== current.code
282
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
283
+ : warning);
284
+ const key = warningKey(current);
285
+ warnings = [...warnings.filter((warning) => warningKey(warning) !== key), warningRecord(current, 'open')];
286
+ }
287
+ return warnings;
288
+ }
289
+ function shouldRunBackfill(receipt) {
290
+ return receipt.lifecycle !== 'done'
291
+ || receipt.taskComment === 'pending'
292
+ || receipt.verification === 'pending'
293
+ || receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open');
294
+ }
229
295
  function issueCapability(receipt, scope) {
230
296
  return {
231
297
  receiptId: receipt.receiptId,
@@ -241,6 +307,21 @@ function capabilityError(code, message) {
241
307
  Object.assign(error, { code });
242
308
  return error;
243
309
  }
310
+ function hasPreparedTaskDocument(repoRoot, taskId) {
311
+ const resolved = resolveTaskRef(taskId, { repoRoot });
312
+ if (!resolved.ok)
313
+ return false;
314
+ if (resolved.state === 'completed')
315
+ return true;
316
+ if (resolved.state !== 'active')
317
+ return false;
318
+ try {
319
+ return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed';
320
+ }
321
+ catch {
322
+ return false;
323
+ }
324
+ }
244
325
  function mutationKeys(value) {
245
326
  return Object.keys(value).sort();
246
327
  }
@@ -310,9 +391,8 @@ function mutationPatch(current, mutation) {
310
391
  : { warningProjection: 'pending', lastError: mutation.error };
311
392
  }
312
393
  function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, consumed) {
313
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
314
- if (!resolved.ok || resolved.state !== 'completed') {
315
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
394
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
395
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
316
396
  }
317
397
  const current = readReceipt(repoRoot, receipt.taskId);
318
398
  if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
@@ -324,11 +404,10 @@ function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability
324
404
  return next;
325
405
  }
326
406
  function applyFinalizationReceiptMutation(repoRoot, receipt, capability, mutation) {
327
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
328
- if (!resolved.ok || resolved.state !== 'completed') {
329
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
407
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
408
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
330
409
  }
331
- return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
410
+ return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
332
411
  }
333
412
  function warningKey(warning) {
334
413
  return `${warning.step}\0${warning.code}\0${warning.target}`;
@@ -339,7 +418,8 @@ function reconcileWarningProjection(repoRoot, taskId, receipt, consumed) {
339
418
  const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
340
419
  for (const warning of warnings) {
341
420
  try {
342
- const projected = projectFinalizationWarning(taskId, warning, { repoRoot });
421
+ const superseded = warning.status === 'resolved' && receipt.warnings.some((item) => item.status === 'open' && item.step === warning.step && item.target === warning.target && item.code !== warning.code);
422
+ const projected = projectFinalizationWarning(taskId, warning, { repoRoot, resolution: superseded ? 'superseded' : 'passed' });
343
423
  if (projected.status === 'failed') {
344
424
  const detail = {
345
425
  code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
@@ -436,16 +516,17 @@ function readDeliverySummary(taskDir, taskId) {
436
516
  }
437
517
  async function syncPendingSummary(input) {
438
518
  let { receipt } = input;
439
- if (receipt.summary !== 'pending') {
440
- return { receipt, step: { status: receipt.summary === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null }, changed: false, error: null };
441
- }
442
519
  try {
443
520
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
444
- if (!resolved.ok || resolved.state !== 'completed')
445
- throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
521
+ if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId))
522
+ throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
523
+ if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
524
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
525
+ return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
526
+ }
446
527
  if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
447
528
  receipt = updateReceipt(input.repoRoot, receipt, {
448
- summary: 'skipped', postSummaryVerification: 'skipped', summarySha256: null, lastError: null
529
+ summary: 'skipped', lastError: null
449
530
  });
450
531
  return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
451
532
  }
@@ -460,8 +541,7 @@ async function syncPendingSummary(input) {
460
541
  const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
461
542
  const warnings = resolveStepWarnings(receipt, 'summary');
462
543
  receipt = updateReceipt(input.repoRoot, receipt, {
463
- summary: skipped ? 'skipped' : 'done', postSummaryVerification: skipped ? 'skipped' : receipt.postSummaryVerification,
464
- summarySha256: skipped ? null : summary.sha256,
544
+ summary: skipped ? 'skipped' : 'done',
465
545
  taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
466
546
  warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
467
547
  warnings,
@@ -488,14 +568,9 @@ async function syncPendingSummary(input) {
488
568
  return { receipt, step: { status: 'blocked', changed: false, error }, changed: false, error };
489
569
  }
490
570
  }
491
- function clearDeliverySummary(taskDir) {
492
- const file = path.join(taskDir, '.delivery-summary.json');
493
- if (fs.existsSync(file))
494
- fs.unlinkSync(file);
495
- }
496
571
  function terminalResult(taskId, receipt, steps, changed, error = null) {
497
572
  const pending = pendingSteps(receipt);
498
- const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary, steps.postSummaryVerification].some((step) => step?.status === 'blocked');
573
+ const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
499
574
  const warnings = openWarnings(receipt);
500
575
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
501
576
  const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
@@ -503,18 +578,18 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
503
578
  status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
504
579
  changed,
505
580
  taskId,
581
+ backfill: steps.backfill ?? null,
506
582
  lifecycle: steps.lifecycle ?? null,
507
583
  taskComment: steps.taskComment ?? null,
508
584
  verification: steps.verification ?? null,
509
585
  summary: steps.summary ?? null,
510
- postSummaryVerification: steps.postSummaryVerification ?? null,
511
586
  completedSteps: completedSteps(receipt),
512
587
  pendingSteps: pending,
513
588
  result: hardError
514
589
  ? (error?.retryable ? 'blocked' : 'failed')
515
- : postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
590
+ : postLifecyclePending
516
591
  ? 'completed_with_warnings'
517
- : pending.length === 0 || postLifecyclePending
592
+ : pending.length === 0
518
593
  ? 'completed'
519
594
  : blocked
520
595
  ? 'blocked'
@@ -523,9 +598,22 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
523
598
  error: hardError ? error : postLifecyclePending ? null : error
524
599
  };
525
600
  }
526
- async function applyUnderLock(request, taskId, options) {
601
+ function preparedResult(taskId, receipt, steps, changed) {
602
+ return {
603
+ status: 'prepared', changed, taskId,
604
+ backfill: steps.backfill ?? null,
605
+ lifecycle: steps.lifecycle ?? null,
606
+ taskComment: steps.taskComment ?? null,
607
+ verification: steps.verification ?? null,
608
+ summary: steps.summary ?? null,
609
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt),
610
+ result: 'prepared', warnings: openWarnings(receipt), error: null
611
+ };
612
+ }
613
+ async function prepareUnderLock(request, taskId, options) {
527
614
  const repoRoot = path.resolve(options.repoRoot);
528
615
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
616
+ const backfill = options.backfill ?? backfillCompletionComments;
529
617
  const commentSync = options.commentSync ?? syncPlatformComment;
530
618
  const verify = options.verify ?? verifyTaskEvent;
531
619
  const consumedCapabilities = new Set();
@@ -562,52 +650,89 @@ async function applyUnderLock(request, taskId, options) {
562
650
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_PREFLIGHT_FAILED', true));
563
651
  }
564
652
  }
565
- let lifecycleResult = null;
566
653
  let changed = false;
567
- try {
568
- const resolved = resolveTaskRef(taskId, { repoRoot });
569
- const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
570
- if (registry && registry.status !== 'valid') {
571
- const detail = {
572
- code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
573
- message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
574
- retryable: false
575
- };
576
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
577
- return terminalResult(taskId, receipt, {
578
- lifecycle: { status: 'failed', changed: false, error: detail },
579
- taskComment: null,
580
- verification: null
581
- }, changed, detail);
654
+ let backfillResult = null;
655
+ let runBackfill = shouldRunBackfill(receipt);
656
+ if (!runBackfill && receipt.lifecycle === 'done') {
657
+ const eligibility = inspectCompletionBackfillEligibility(taskId, { cwd: repoRoot });
658
+ if (eligibility.status === 'failed')
659
+ return failed(taskId, eligibility.error, {
660
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
661
+ });
662
+ runBackfill = eligibility.eligible;
663
+ }
664
+ if (receipt.lifecycle === 'done' && runBackfill && receipt.summary === 'done') {
665
+ receipt = updateReceipt(repoRoot, receipt, { summary: 'pending' });
666
+ }
667
+ if (runBackfill)
668
+ try {
669
+ const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
670
+ backfillResult = commentStep(result);
671
+ const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
672
+ if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
673
+ const detail = backfillResult.error ?? {
674
+ code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
675
+ };
676
+ if (receipt.lifecycle !== 'done')
677
+ return failed(taskId, detail, {
678
+ backfill: backfillResult,
679
+ completedSteps: completedSteps(receipt),
680
+ pendingSteps: pendingSteps(receipt)
681
+ });
682
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
683
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
684
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
685
+ if (!detail.retryable)
686
+ return failed(taskId, detail, {
687
+ changed, backfill: backfillResult, completedSteps: completedSteps(receipt),
688
+ pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
689
+ });
690
+ return terminalResult(taskId, receipt, { backfill: backfillResult }, changed, detail);
691
+ }
692
+ changed = result.changed;
693
+ if (receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open')) {
694
+ const warnings = receipt.warnings.map((warning) => warning.step === 'backfill' && warning.status === 'open'
695
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
696
+ : warning);
697
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: null });
698
+ }
699
+ if (result.changed && receipt.taskComment !== 'pending') {
700
+ receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
701
+ }
582
702
  }
583
- const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
584
- if (shortIds?.has(taskId)) {
585
- const detail = {
586
- code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
587
- message: 'completed task still has an active short-id registry entry',
588
- retryable: false
589
- };
590
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
591
- return terminalResult(taskId, receipt, {
592
- lifecycle: { status: 'failed', changed: false, error: detail },
593
- taskComment: null,
594
- verification: null
595
- }, changed, detail);
703
+ catch (error) {
704
+ const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
705
+ if (receipt.lifecycle === 'done') {
706
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
707
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
708
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
709
+ return terminalResult(taskId, receipt, {
710
+ backfill: { status: 'blocked', changed: false, error: detail }
711
+ }, changed, detail);
712
+ }
713
+ return failed(taskId, detail, {
714
+ backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
715
+ completedSteps: completedSteps(receipt),
716
+ pendingSteps: pendingSteps(receipt)
717
+ });
596
718
  }
597
- const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
598
- if (lifecycleDone) {
719
+ else
720
+ backfillResult = { status: 'no-op', changed: false, error: null };
721
+ let lifecycleResult = null;
722
+ try {
723
+ const resolved = resolveTaskRef(taskId, { repoRoot });
724
+ if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
599
725
  lifecycleResult = { status: 'no-op', changed: false, error: null };
600
726
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
601
727
  }
602
728
  else {
603
- const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
729
+ const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
604
730
  lifecycleResult = lifecycleStep(result);
605
731
  if (result.status !== 'applied' && result.status !== 'no-op') {
606
732
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
607
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
733
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
608
734
  }
609
- changed = result.changed;
610
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
735
+ changed = changed || result.changed;
611
736
  }
612
737
  }
613
738
  catch (error) {
@@ -616,31 +741,30 @@ async function applyUnderLock(request, taskId, options) {
616
741
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
617
742
  }
618
743
  catch { /* preserve the primary error */ }
619
- return failed(taskId, detail, { lifecycle: lifecycleResult });
744
+ return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
620
745
  }
621
746
  let taskComment = null;
622
747
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
623
- const initialComment = await syncPendingTaskComment({
624
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
625
- });
626
- receipt = initialComment.receipt;
627
- taskComment = initialComment.step;
628
- changed = changed || initialComment.changed;
629
- if (initialComment.error) {
630
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
631
- }
632
748
  let verification = null;
633
- try {
634
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
635
- const verificationPending = receipt.verification === 'pending';
636
- const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
637
- verification = verificationStep(result);
638
- if (result.status === 'pass') {
639
- if (verificationPending) {
640
- const capability = issueCapability(receipt, 'verification');
641
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
642
- scope: 'verification', operation: 'succeeded'
643
- }, consumedCapabilities);
749
+ if (receipt.verification !== 'pending') {
750
+ verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
751
+ }
752
+ else {
753
+ try {
754
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
755
+ const verificationState = resolveTaskRef(taskId, { repoRoot });
756
+ const verificationEvent = verificationState.ok && verificationState.state === 'completed'
757
+ ? 'complete-task.completed'
758
+ : 'complete-task.prepared';
759
+ const result = await verify({ taskRef: taskId, event: verificationEvent }, { repoRoot });
760
+ verification = verificationStep(result);
761
+ if (result.status === 'pass') {
762
+ const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
763
+ const warnings = applyVerificationWarnings(receipt, result);
764
+ receipt = updateReceipt(repoRoot, receipt, {
765
+ verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
766
+ warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
767
+ });
644
768
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
645
769
  const finalComment = await syncPendingTaskComment({
646
770
  repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
@@ -650,176 +774,101 @@ async function applyUnderLock(request, taskId, options) {
650
774
  taskComment = finalComment.step;
651
775
  changed = changed || finalComment.changed;
652
776
  if (finalComment.error) {
653
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
777
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
654
778
  }
655
779
  }
656
780
  else {
657
- receipt = updateReceipt(repoRoot, receipt, { lastError: null });
658
- }
659
- }
660
- else {
661
- const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
662
- if (verificationPending) {
663
- const capability = issueCapability(receipt, 'verification');
664
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
665
- scope: 'verification', operation: 'failed', error: detail
666
- }, consumedCapabilities);
667
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
668
- }
669
- else {
670
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
781
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
782
+ const warnings = applyVerificationWarnings(receipt, result);
671
783
  receipt = updateReceipt(repoRoot, receipt, {
672
784
  verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
673
785
  });
674
786
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
675
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
787
+ const warningComment = await syncPendingTaskComment({
788
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
789
+ });
676
790
  receipt = warningComment.receipt;
677
- if (warningComment.step.status !== 'no-op')
678
- taskComment = warningComment.step;
791
+ taskComment = warningComment.step;
679
792
  changed = changed || warningComment.changed;
793
+ if (warningComment.error) {
794
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
795
+ }
796
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
680
797
  }
681
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
682
798
  }
683
- }
684
- catch (error) {
685
- const detail = errorOf(error, 'VERIFY_FAILED', true);
686
- try {
687
- if (receipt.verification === 'pending') {
688
- const capability = issueCapability(receipt, 'verification');
689
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
690
- scope: 'verification', operation: 'failed', error: detail
691
- }, consumedCapabilities);
692
- }
693
- else {
799
+ catch (error) {
800
+ const detail = errorOf(error, 'VERIFY_FAILED', true);
801
+ try {
694
802
  const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
695
803
  receipt = updateReceipt(repoRoot, receipt, {
696
804
  verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
697
805
  });
698
- }
699
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
700
- }
701
- catch (writeError) {
702
- const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
703
- const hardError = {
704
- code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
705
- message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
706
- retryable: true
707
- };
708
- return terminalResult(taskId, receipt, {
709
- lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
710
- }, changed, hardError);
711
- }
712
- try {
713
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
714
- receipt = warningComment.receipt;
715
- if (warningComment.step.status !== 'no-op')
716
- taskComment = warningComment.step;
717
- changed = changed || warningComment.changed;
718
- }
719
- catch { /* preserve the primary error */ }
720
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
721
- }
722
- const summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
723
- receipt = summary.receipt;
724
- changed = changed || summary.changed;
725
- if (summary.error) {
726
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
727
- const warningComment = await syncPendingTaskComment({
728
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
729
- });
730
- receipt = warningComment.receipt;
731
- if (warningComment.step.status !== 'no-op')
732
- taskComment = warningComment.step;
733
- changed = changed || warningComment.changed;
734
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
735
- }
736
- let postSummaryVerification = null;
737
- if (receipt.postSummaryVerification === 'skipped') {
738
- postSummaryVerification = { status: 'skipped', changed: false, error: null };
739
- }
740
- else if (receipt.postSummaryVerification !== 'pending') {
741
- const replay = await commentSync(taskId, {
742
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
743
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
744
- });
745
- postSummaryVerification = commentStep(replay);
746
- if (replay.status !== 'applied' && replay.status !== 'no-op') {
747
- const error = postSummaryVerification.error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary replay verification failed', retryable: true };
748
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
749
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
750
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
751
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
752
- }
753
- }
754
- else {
755
- try {
756
- const summaryVerification = await commentSync(taskId, {
757
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
758
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
759
- });
760
- if (summaryVerification.status !== 'applied' && summaryVerification.status !== 'no-op') {
761
- const error = commentStep(summaryVerification).error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary verification failed', retryable: true };
762
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
763
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
764
806
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
765
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
766
- receipt = warningComment.receipt;
767
- if (warningComment.step.status !== 'no-op')
768
- taskComment = warningComment.step;
769
- changed = changed || warningComment.changed;
770
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: commentStep(summaryVerification) }, changed, error);
771
807
  }
772
- const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
773
- postSummaryVerification = verificationStep(result);
774
- if (result.status === 'pass') {
775
- const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'post-summary-verification' && warning.status === 'open');
776
- const warnings = resolveStepWarnings(receipt, 'post-summary-verification');
777
- receipt = updateReceipt(repoRoot, receipt, {
778
- postSummaryVerification: 'done', taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
779
- warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done', warnings, lastError: null
780
- });
781
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
782
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
783
- receipt = warningComment.receipt;
784
- if (warningComment.step.status !== 'no-op')
785
- taskComment = warningComment.step;
786
- changed = changed || warningComment.changed;
808
+ catch (writeError) {
809
+ const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
810
+ const hardError = {
811
+ code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
812
+ message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
813
+ retryable: true
814
+ };
815
+ return terminalResult(taskId, receipt, {
816
+ backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
817
+ }, changed, hardError);
787
818
  }
788
- else {
789
- const error = postSummaryVerification.error ?? { code: 'VERIFY_FAILED', message: 'post-summary verification failed', retryable: true };
790
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
791
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
792
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
819
+ try {
793
820
  const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
794
821
  receipt = warningComment.receipt;
795
822
  if (warningComment.step.status !== 'no-op')
796
823
  taskComment = warningComment.step;
797
824
  changed = changed || warningComment.changed;
798
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
799
825
  }
800
- }
801
- catch (error) {
802
- const detail = errorOf(error, 'POST_SUMMARY_VERIFY_FAILED', true);
803
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', detail), 'open');
804
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail });
805
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
806
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
807
- receipt = warningComment.receipt;
808
- if (warningComment.step.status !== 'no-op')
809
- taskComment = warningComment.step;
810
- changed = changed || warningComment.changed;
811
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
826
+ catch { /* preserve the primary error */ }
827
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
812
828
  }
813
829
  }
830
+ let summary = receipt.summary === 'pending'
831
+ ? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
832
+ : { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
833
+ receipt = summary.receipt;
834
+ changed = changed || summary.changed;
835
+ if (summary.error) {
836
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
837
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
838
+ }
814
839
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
815
- if (receipt.summary !== 'pending' && receipt.postSummaryVerification !== 'pending' && receipt.warningProjection === 'done' && openWarnings(receipt).length === 0) {
816
- const resolved = resolveTaskRef(taskId, { repoRoot });
817
- if (resolved.ok && resolved.state === 'completed')
818
- clearDeliverySummary(resolved.taskDir);
840
+ if (receipt.taskComment === 'pending') {
841
+ const finalComment = await syncPendingTaskComment({
842
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
843
+ });
844
+ receipt = finalComment.receipt;
845
+ taskComment = finalComment.step;
846
+ changed = changed || finalComment.changed;
847
+ if (finalComment.error) {
848
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, finalComment.error);
849
+ }
819
850
  }
820
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
851
+ return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
821
852
  }
822
- async function applyTaskFinalization(request, options) {
853
+ async function prepareTaskFinalization(request, options) {
854
+ if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
855
+ return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
856
+ }
857
+ const repoRoot = path.resolve(options.repoRoot);
858
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot });
859
+ if (!resolved.ok)
860
+ return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
861
+ try {
862
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
863
+ }
864
+ catch (error) {
865
+ const detail = error instanceof TaskExecutionLockError
866
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
867
+ : errorOf(error, 'TASK_FINALIZATION_FAILED');
868
+ return failed(resolved.taskId, detail);
869
+ }
870
+ }
871
+ async function commitPreparedTaskFinalization(request, options) {
823
872
  if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
824
873
  return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
825
874
  }
@@ -828,7 +877,37 @@ async function applyTaskFinalization(request, options) {
828
877
  if (!resolved.ok)
829
878
  return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
830
879
  try {
831
- return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
880
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
881
+ const receipt = readReceipt(repoRoot, resolved.taskId);
882
+ if (!receipt)
883
+ return failed(resolved.taskId, {
884
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
885
+ });
886
+ if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
887
+ return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
888
+ }
889
+ if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
890
+ return failed(resolved.taskId, {
891
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
892
+ }, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
893
+ }
894
+ const registry = inspectShortIdRegistry(repoRoot);
895
+ if (registry.status !== 'valid')
896
+ return failed(resolved.taskId, {
897
+ code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
898
+ message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
899
+ retryable: false
900
+ });
901
+ const lifecycle = options.lifecycle ?? applyTaskLifecycle;
902
+ const result = lifecycle({ taskRef: resolved.taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
903
+ const step = lifecycleStep(result);
904
+ if (result.status !== 'applied' && result.status !== 'no-op') {
905
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
906
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
907
+ }
908
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
909
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
910
+ });
832
911
  }
833
912
  catch (error) {
834
913
  const detail = error instanceof TaskExecutionLockError
@@ -837,5 +916,11 @@ async function applyTaskFinalization(request, options) {
837
916
  return failed(resolved.taskId, detail);
838
917
  }
839
918
  }
840
- export { applyFinalizationReceiptMutation, applyTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
919
+ async function applyTaskFinalization(request, options) {
920
+ const prepared = await prepareTaskFinalization(request, options);
921
+ if (prepared.status !== 'prepared')
922
+ return prepared;
923
+ return commitPreparedTaskFinalization(request, options);
924
+ }
925
+ export { applyFinalizationReceiptMutation, commitPreparedTaskFinalization, applyTaskFinalization, prepareTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
841
926
  //# sourceMappingURL=finalization.js.map