@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
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
4
4
 
5
5
  import { syncPlatformComment } from '../platform/issue-comments.ts';
6
6
  import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
7
+ import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.ts';
7
8
  import type { PlatformResult } from '../platform/types.ts';
8
9
  import { taskIssueIdentity } from '../platform/task-identities.ts';
9
10
  import { parseTaskFrontmatter } from './frontmatter.ts';
@@ -25,8 +26,8 @@ import {
25
26
  type OperationWarningSeverity
26
27
  } from './operation-outcome.ts';
27
28
 
28
- const RECEIPT_VERSION = 3 as const;
29
- const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary', 'post-summary-verification'] as const;
29
+ const RECEIPT_VERSION = 4 as const;
30
+ const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
30
31
  type FinalizationStep = typeof FINALIZATION_STEPS[number];
31
32
  type FinalizationStepState = 'pending' | 'done' | 'skipped';
32
33
  type FinalizationError = { code: string; message: string; retryable: boolean };
@@ -61,6 +62,7 @@ type TaskFinalizationOptions = Readonly<{
61
62
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
62
63
  metadataProvider?: TaskLifecycleOptions['metadataProvider'];
63
64
  lifecycle?: typeof applyTaskLifecycle;
65
+ backfill?: typeof backfillCompletionComments;
64
66
  commentSync?: typeof syncPlatformComment;
65
67
  verify?: typeof verifyTaskEvent;
66
68
  preflight?: typeof verifyTaskEvent;
@@ -76,10 +78,8 @@ type TaskFinalizationReceipt = Readonly<{
76
78
  taskComment: FinalizationStepState;
77
79
  verification: FinalizationStepState;
78
80
  summary: FinalizationStepState;
79
- postSummaryVerification: FinalizationStepState;
80
81
  warningProjection: WarningProjectionState;
81
82
  warnings: readonly FinalizationWarning[];
82
- summarySha256: string | null;
83
83
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
84
84
  updatedAt: string;
85
85
  lastError: FinalizationError | null;
@@ -101,17 +101,17 @@ type TaskCommentSyncOutcome = Readonly<{
101
101
  }>;
102
102
 
103
103
  type TaskFinalizationResult = Readonly<{
104
- status: 'completed' | 'failed' | 'blocked';
104
+ status: 'prepared' | 'completed' | 'failed' | 'blocked';
105
105
  changed: boolean;
106
106
  taskId: string | null;
107
+ backfill: TaskFinalizationStep | null;
107
108
  lifecycle: TaskFinalizationStep | null;
108
109
  taskComment: TaskFinalizationStep | null;
109
110
  verification: TaskFinalizationStep | null;
110
111
  summary: TaskFinalizationStep | null;
111
- postSummaryVerification: TaskFinalizationStep | null;
112
112
  completedSteps: readonly FinalizationStep[];
113
113
  pendingSteps: readonly FinalizationStep[];
114
- result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
114
+ result: 'prepared' | 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
115
115
  warnings: readonly OperationWarning[];
116
116
  error: FinalizationError | null;
117
117
  }>;
@@ -144,11 +144,11 @@ function failed(
144
144
  status: error.retryable ? 'blocked' : 'failed',
145
145
  changed: false,
146
146
  taskId,
147
+ backfill: null,
147
148
  lifecycle: null,
148
149
  taskComment: null,
149
150
  verification: null,
150
151
  summary: null,
151
- postSummaryVerification: null,
152
152
  completedSteps: [],
153
153
  pendingSteps: [...FINALIZATION_STEPS],
154
154
  result: error.retryable ? 'blocked' : 'failed',
@@ -169,10 +169,8 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
169
169
  taskComment: 'pending',
170
170
  verification: 'pending',
171
171
  summary: 'pending',
172
- postSummaryVerification: 'pending',
173
172
  warningProjection: 'done',
174
173
  warnings: [],
175
- summarySha256: null,
176
174
  ...(controlBinding ? { controlBinding } : {}),
177
175
  updatedAt: now(),
178
176
  lastError: null
@@ -208,10 +206,8 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
208
206
  || !states.includes(String(receipt.taskComment))
209
207
  || !states.includes(String(receipt.verification))
210
208
  || !states.includes(String(receipt.summary))
211
- || !states.includes(String(receipt.postSummaryVerification))
212
209
  || !['pending', 'done'].includes(String(receipt.warningProjection))
213
210
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
214
- || (receipt.summarySha256 !== null && (typeof receipt.summarySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(receipt.summarySha256)))
215
211
  || typeof receipt.updatedAt !== 'string'
216
212
  || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
217
213
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
@@ -250,7 +246,7 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
250
246
  function updateReceipt(
251
247
  repoRoot: string,
252
248
  receipt: TaskFinalizationReceipt,
253
- patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification' | 'warningProjection' | 'warnings' | 'summarySha256' | 'controlBinding' | 'lastError'>>
249
+ patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'warningProjection' | 'warnings' | 'controlBinding' | 'lastError'>>
254
250
  ): TaskFinalizationReceipt {
255
251
  const current = readReceipt(repoRoot, receipt.taskId);
256
252
  if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
@@ -272,7 +268,6 @@ function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
272
268
 
273
269
  function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
274
270
  if (step === 'task-comment') return receipt.taskComment;
275
- if (step === 'post-summary-verification') return receipt.postSummaryVerification;
276
271
  return receipt[step];
277
272
  }
278
273
 
@@ -319,7 +314,7 @@ function verificationFailure(result: TaskVerificationResult): FinalizationError
319
314
  }
320
315
 
321
316
  function warningFromError(
322
- step: FinalizationStep,
317
+ step: string,
323
318
  error: FinalizationError,
324
319
  severity: OperationWarningSeverity = 'ACTION_REQUIRED'
325
320
  ): OperationWarning {
@@ -362,6 +357,76 @@ function resolveStepWarnings(receipt: TaskFinalizationReceipt, step: Finalizatio
362
357
  : warning);
363
358
  }
364
359
 
360
+ function verificationWarnings(result: TaskVerificationResult): readonly OperationWarning[] {
361
+ const warnings: OperationWarning[] = [];
362
+ for (const invocation of result.invocations) {
363
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
364
+ for (const value of checks) {
365
+ if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
366
+ const check = value as Record<string, unknown>;
367
+ const status = check.effectiveStatus ?? check.status;
368
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
369
+ if (!target || status === 'pass') continue;
370
+ const blocked = status === 'blocked';
371
+ const message = [check.reason, check.message, check.action]
372
+ .filter((item): item is string => typeof item === 'string' && item.length > 0)
373
+ .join(' - ');
374
+ warnings.push({
375
+ step: 'verification', target, code: blocked ? 'CHECK_BLOCKED' : 'CHECK_FAILED',
376
+ retryable: blocked, severity: 'ACTION_REQUIRED',
377
+ message: message || `verification ${String(status)}`
378
+ });
379
+ }
380
+ }
381
+ return mergeOperationWarnings(warnings);
382
+ }
383
+
384
+ function applyVerificationWarnings(
385
+ receipt: TaskFinalizationReceipt,
386
+ result: TaskVerificationResult
387
+ ): readonly FinalizationWarning[] {
388
+ if (result.status === 'pass') return resolveStepWarnings(receipt, 'verification');
389
+ const observed = new Map<string, 'pass' | OperationWarning>();
390
+ for (const invocation of result.invocations) {
391
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
392
+ for (const value of checks) {
393
+ if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
394
+ const check = value as Record<string, unknown>;
395
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
396
+ if (!target) continue;
397
+ const status = check.effectiveStatus ?? check.status;
398
+ if (status === 'pass') observed.set(target, 'pass');
399
+ }
400
+ }
401
+ for (const warning of verificationWarnings(result)) observed.set(warning.target, warning);
402
+ if (observed.size === 0) {
403
+ return replaceWarning(receipt, warningFromError('verification', verificationFailure(result)), 'open');
404
+ }
405
+ let warnings = [...receipt.warnings];
406
+ for (const [target, current] of observed) {
407
+ if (current === 'pass') {
408
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target && warning.status === 'open'
409
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
410
+ : warning);
411
+ continue;
412
+ }
413
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target
414
+ && warning.status === 'open' && warning.code !== current.code
415
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
416
+ : warning);
417
+ const key = warningKey(current);
418
+ warnings = [...warnings.filter((warning) => warningKey(warning) !== key), warningRecord(current, 'open')];
419
+ }
420
+ return warnings;
421
+ }
422
+
423
+ function shouldRunBackfill(receipt: TaskFinalizationReceipt): boolean {
424
+ return receipt.lifecycle !== 'done'
425
+ || receipt.taskComment === 'pending'
426
+ || receipt.verification === 'pending'
427
+ || receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open');
428
+ }
429
+
365
430
  function issueCapability(receipt: TaskFinalizationReceipt, scope: Exclude<FinalizationStep, 'lifecycle'> | 'warning-projection'): FinalizationCapability {
366
431
  return {
367
432
  receiptId: receipt.receiptId,
@@ -379,6 +444,15 @@ function capabilityError(code: 'FINALIZATION_CAPABILITY_STALE' | 'FINALIZATION_S
379
444
  return error;
380
445
  }
381
446
 
447
+ function hasPreparedTaskDocument(repoRoot: string, taskId: string): boolean {
448
+ const resolved = resolveTaskRef(taskId, { repoRoot });
449
+ if (!resolved.ok) return false;
450
+ if (resolved.state === 'completed') return true;
451
+ if (resolved.state !== 'active') return false;
452
+ try { return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed'; }
453
+ catch { return false; }
454
+ }
455
+
382
456
  function mutationKeys(value: FinalizationMutation): string[] {
383
457
  return Object.keys(value).sort();
384
458
  }
@@ -456,9 +530,8 @@ function applyFinalizationReceiptMutationUnderLock(
456
530
  mutation: FinalizationMutation,
457
531
  consumed: Set<string>
458
532
  ): TaskFinalizationReceipt {
459
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
460
- if (!resolved.ok || resolved.state !== 'completed') {
461
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
533
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
534
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
462
535
  }
463
536
  const current = readReceipt(repoRoot, receipt.taskId);
464
537
  if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
@@ -476,11 +549,10 @@ function applyFinalizationReceiptMutation(
476
549
  capability: FinalizationCapability,
477
550
  mutation: FinalizationMutation
478
551
  ): TaskFinalizationReceipt {
479
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
480
- if (!resolved.ok || resolved.state !== 'completed') {
481
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
552
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
553
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
482
554
  }
483
- return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.receipt-mutation', () =>
555
+ return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () =>
484
556
  applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set<string>())
485
557
  );
486
558
  }
@@ -494,7 +566,10 @@ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: T
494
566
  const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
495
567
  for (const warning of warnings) {
496
568
  try {
497
- const projected = projectFinalizationWarning(taskId, warning, { repoRoot });
569
+ const superseded = warning.status === 'resolved' && receipt.warnings.some((item) =>
570
+ item.status === 'open' && item.step === warning.step && item.target === warning.target && item.code !== warning.code
571
+ );
572
+ const projected = projectFinalizationWarning(taskId, warning, { repoRoot, resolution: superseded ? 'superseded' : 'passed' });
498
573
  if (projected.status === 'failed') {
499
574
  const detail: FinalizationError = {
500
575
  code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
@@ -598,15 +673,16 @@ async function syncPendingSummary(input: {
598
673
  commentSync: typeof syncPlatformComment;
599
674
  }): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
600
675
  let { receipt } = input;
601
- if (receipt.summary !== 'pending') {
602
- return { receipt, step: { status: receipt.summary === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null }, changed: false, error: null };
603
- }
604
676
  try {
605
677
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
606
- if (!resolved.ok || resolved.state !== 'completed') throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
678
+ if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId)) throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
679
+ if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
680
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
681
+ return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
682
+ }
607
683
  if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
608
684
  receipt = updateReceipt(input.repoRoot, receipt, {
609
- summary: 'skipped', postSummaryVerification: 'skipped', summarySha256: null, lastError: null
685
+ summary: 'skipped', lastError: null
610
686
  });
611
687
  return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
612
688
  }
@@ -621,8 +697,7 @@ async function syncPendingSummary(input: {
621
697
  const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
622
698
  const warnings = resolveStepWarnings(receipt, 'summary');
623
699
  receipt = updateReceipt(input.repoRoot, receipt, {
624
- summary: skipped ? 'skipped' : 'done', postSummaryVerification: skipped ? 'skipped' : receipt.postSummaryVerification,
625
- summarySha256: skipped ? null : summary.sha256,
700
+ summary: skipped ? 'skipped' : 'done',
626
701
  taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
627
702
  warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
628
703
  warnings,
@@ -648,20 +723,15 @@ async function syncPendingSummary(input: {
648
723
  }
649
724
  }
650
725
 
651
- function clearDeliverySummary(taskDir: string): void {
652
- const file = path.join(taskDir, '.delivery-summary.json');
653
- if (fs.existsSync(file)) fs.unlinkSync(file);
654
- }
655
-
656
726
  function terminalResult(
657
727
  taskId: string,
658
728
  receipt: TaskFinalizationReceipt,
659
- steps: Partial<Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification'>>,
729
+ steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
660
730
  changed: boolean,
661
731
  error: FinalizationError | null = null
662
732
  ): TaskFinalizationResult {
663
733
  const pending = pendingSteps(receipt);
664
- const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary, steps.postSummaryVerification].some((step) => step?.status === 'blocked');
734
+ const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
665
735
  const warnings = openWarnings(receipt);
666
736
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
667
737
  const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
@@ -669,18 +739,18 @@ function terminalResult(
669
739
  status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
670
740
  changed,
671
741
  taskId,
742
+ backfill: steps.backfill ?? null,
672
743
  lifecycle: steps.lifecycle ?? null,
673
744
  taskComment: steps.taskComment ?? null,
674
745
  verification: steps.verification ?? null,
675
746
  summary: steps.summary ?? null,
676
- postSummaryVerification: steps.postSummaryVerification ?? null,
677
747
  completedSteps: completedSteps(receipt),
678
748
  pendingSteps: pending,
679
749
  result: hardError
680
750
  ? (error?.retryable ? 'blocked' : 'failed')
681
- : postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
751
+ : postLifecyclePending
682
752
  ? 'completed_with_warnings'
683
- : pending.length === 0 || postLifecyclePending
753
+ : pending.length === 0
684
754
  ? 'completed'
685
755
  : blocked
686
756
  ? 'blocked'
@@ -690,13 +760,32 @@ function terminalResult(
690
760
  };
691
761
  }
692
762
 
693
- async function applyUnderLock(
763
+ function preparedResult(
764
+ taskId: string,
765
+ receipt: TaskFinalizationReceipt,
766
+ steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
767
+ changed: boolean
768
+ ): TaskFinalizationResult {
769
+ return {
770
+ status: 'prepared', changed, taskId,
771
+ backfill: steps.backfill ?? null,
772
+ lifecycle: steps.lifecycle ?? null,
773
+ taskComment: steps.taskComment ?? null,
774
+ verification: steps.verification ?? null,
775
+ summary: steps.summary ?? null,
776
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt),
777
+ result: 'prepared', warnings: openWarnings(receipt), error: null
778
+ };
779
+ }
780
+
781
+ async function prepareUnderLock(
694
782
  request: TaskFinalizationRequest,
695
783
  taskId: string,
696
784
  options: TaskFinalizationOptions
697
785
  ): Promise<TaskFinalizationResult> {
698
786
  const repoRoot = path.resolve(options.repoRoot);
699
787
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
788
+ const backfill = options.backfill ?? backfillCompletionComments;
700
789
  const commentSync = options.commentSync ?? syncPlatformComment;
701
790
  const verify = options.verify ?? verifyTaskEvent;
702
791
  const consumedCapabilities = new Set<string>();
@@ -736,136 +825,152 @@ async function applyUnderLock(
736
825
  }
737
826
  }
738
827
 
739
- let lifecycleResult: TaskFinalizationStep | null = null;
740
828
  let changed = false;
741
- try {
742
- const resolved = resolveTaskRef(taskId, { repoRoot });
743
- const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
744
- if (registry && registry.status !== 'valid') {
745
- const detail: FinalizationError = {
746
- code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
747
- message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
748
- retryable: false
829
+ let backfillResult: TaskFinalizationStep | null = null;
830
+ let runBackfill = shouldRunBackfill(receipt);
831
+ if (!runBackfill && receipt.lifecycle === 'done') {
832
+ const eligibility = inspectCompletionBackfillEligibility(taskId, { cwd: repoRoot });
833
+ if (eligibility.status === 'failed') return failed(taskId, eligibility.error!, {
834
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
835
+ });
836
+ runBackfill = eligibility.eligible;
837
+ }
838
+ if (receipt.lifecycle === 'done' && runBackfill && receipt.summary === 'done') {
839
+ receipt = updateReceipt(repoRoot, receipt, { summary: 'pending' });
840
+ }
841
+ if (runBackfill) try {
842
+ const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
843
+ backfillResult = commentStep(result);
844
+ const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
845
+ if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
846
+ const detail = backfillResult.error ?? {
847
+ code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
749
848
  };
750
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
751
- return terminalResult(taskId, receipt, {
752
- lifecycle: { status: 'failed', changed: false, error: detail },
753
- taskComment: null,
754
- verification: null
755
- }, changed, detail);
849
+ if (receipt.lifecycle !== 'done') return failed(taskId, detail, {
850
+ backfill: backfillResult,
851
+ completedSteps: completedSteps(receipt),
852
+ pendingSteps: pendingSteps(receipt)
853
+ });
854
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
855
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
856
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
857
+ if (!detail.retryable) return failed(taskId, detail, {
858
+ changed, backfill: backfillResult, completedSteps: completedSteps(receipt),
859
+ pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
860
+ });
861
+ return terminalResult(taskId, receipt, { backfill: backfillResult }, changed, detail);
756
862
  }
757
- const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
758
- if (shortIds?.has(taskId)) {
759
- const detail: FinalizationError = {
760
- code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
761
- message: 'completed task still has an active short-id registry entry',
762
- retryable: false
763
- };
764
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
863
+ changed = result.changed;
864
+ if (receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open')) {
865
+ const warnings = receipt.warnings.map((warning) => warning.step === 'backfill' && warning.status === 'open'
866
+ ? { ...warning, status: 'resolved' as const, resolvedAt: now() }
867
+ : warning);
868
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: null });
869
+ }
870
+ if (result.changed && receipt.taskComment !== 'pending') {
871
+ receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
872
+ }
873
+ } catch (error) {
874
+ const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
875
+ if (receipt.lifecycle === 'done') {
876
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
877
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
878
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
765
879
  return terminalResult(taskId, receipt, {
766
- lifecycle: { status: 'failed', changed: false, error: detail },
767
- taskComment: null,
768
- verification: null
880
+ backfill: { status: 'blocked', changed: false, error: detail }
769
881
  }, changed, detail);
770
882
  }
771
- const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
772
- if (lifecycleDone) {
883
+ return failed(taskId, detail, {
884
+ backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
885
+ completedSteps: completedSteps(receipt),
886
+ pendingSteps: pendingSteps(receipt)
887
+ });
888
+ } else backfillResult = { status: 'no-op', changed: false, error: null };
889
+
890
+ let lifecycleResult: TaskFinalizationStep | null = null;
891
+ try {
892
+ const resolved = resolveTaskRef(taskId, { repoRoot });
893
+ if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
773
894
  lifecycleResult = { status: 'no-op', changed: false, error: null };
774
895
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
775
896
  } else {
776
897
  const result = lifecycle(
777
898
  { taskRef: taskId, intent: 'complete', agent: request.agent },
778
- { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
899
+ { repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
779
900
  );
780
901
  lifecycleResult = lifecycleStep(result);
781
902
  if (result.status !== 'applied' && result.status !== 'no-op') {
782
903
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
783
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
904
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
784
905
  }
785
- changed = result.changed;
786
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
906
+ changed = changed || result.changed;
787
907
  }
788
908
  } catch (error) {
789
909
  const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
790
910
  try { receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail }); } catch { /* preserve the primary error */ }
791
- return failed(taskId, detail, { lifecycle: lifecycleResult });
911
+ return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
792
912
  }
793
913
 
794
914
  let taskComment: TaskFinalizationStep | null = null;
795
915
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
796
- const initialComment = await syncPendingTaskComment({
797
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
798
- });
799
- receipt = initialComment.receipt;
800
- taskComment = initialComment.step;
801
- changed = changed || initialComment.changed;
802
- if (initialComment.error) {
803
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
804
- }
805
916
 
806
917
  let verification: TaskFinalizationStep | null = null;
807
- try {
918
+ if (receipt.verification !== 'pending') {
919
+ verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
920
+ } else {
921
+ try {
808
922
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
809
- const verificationPending = receipt.verification === 'pending';
923
+ const verificationState = resolveTaskRef(taskId, { repoRoot });
924
+ const verificationEvent = verificationState.ok && verificationState.state === 'completed'
925
+ ? 'complete-task.completed'
926
+ : 'complete-task.prepared';
810
927
  const result = await verify(
811
- { taskRef: taskId, event: 'complete-task.completed' },
928
+ { taskRef: taskId, event: verificationEvent },
812
929
  { repoRoot }
813
930
  );
814
931
  verification = verificationStep(result);
815
932
  if (result.status === 'pass') {
816
- if (verificationPending) {
817
- const capability = issueCapability(receipt, 'verification');
818
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
819
- scope: 'verification', operation: 'succeeded'
820
- }, consumedCapabilities);
821
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
822
- const finalComment = await syncPendingTaskComment({
823
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
824
- });
825
- receipt = finalComment.receipt;
826
- if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
827
- changed = changed || finalComment.changed;
828
- if (finalComment.error) {
829
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
830
- }
831
- } else {
832
- receipt = updateReceipt(repoRoot, receipt, { lastError: null });
933
+ const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
934
+ const warnings = applyVerificationWarnings(receipt, result);
935
+ receipt = updateReceipt(repoRoot, receipt, {
936
+ verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
937
+ warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
938
+ });
939
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
940
+ const finalComment = await syncPendingTaskComment({
941
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
942
+ });
943
+ receipt = finalComment.receipt;
944
+ if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
945
+ changed = changed || finalComment.changed;
946
+ if (finalComment.error) {
947
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
833
948
  }
834
949
  } else {
835
950
  const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
836
- if (verificationPending) {
837
- const capability = issueCapability(receipt, 'verification');
838
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
839
- scope: 'verification', operation: 'failed', error: detail
840
- }, consumedCapabilities);
841
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
842
- } else {
843
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
844
- receipt = updateReceipt(repoRoot, receipt, {
845
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
846
- });
847
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
848
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
849
- receipt = warningComment.receipt;
850
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
851
- changed = changed || warningComment.changed;
951
+ const warnings = applyVerificationWarnings(receipt, result);
952
+ receipt = updateReceipt(repoRoot, receipt, {
953
+ verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
954
+ });
955
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
956
+ const warningComment = await syncPendingTaskComment({
957
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
958
+ });
959
+ receipt = warningComment.receipt;
960
+ taskComment = warningComment.step;
961
+ changed = changed || warningComment.changed;
962
+ if (warningComment.error) {
963
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
852
964
  }
853
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
965
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
854
966
  }
855
967
  } catch (error) {
856
968
  const detail = errorOf(error, 'VERIFY_FAILED', true);
857
969
  try {
858
- if (receipt.verification === 'pending') {
859
- const capability = issueCapability(receipt, 'verification');
860
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
861
- scope: 'verification', operation: 'failed', error: detail
862
- }, consumedCapabilities);
863
- } else {
864
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
865
- receipt = updateReceipt(repoRoot, receipt, {
866
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
867
- });
868
- }
970
+ const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
971
+ receipt = updateReceipt(repoRoot, receipt, {
972
+ verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
973
+ });
869
974
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
870
975
  } catch (writeError) {
871
976
  const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
@@ -875,7 +980,7 @@ async function applyUnderLock(
875
980
  retryable: true
876
981
  };
877
982
  return terminalResult(taskId, receipt, {
878
- lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
983
+ backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
879
984
  }, changed, hardError);
880
985
  }
881
986
  try {
@@ -884,102 +989,52 @@ async function applyUnderLock(
884
989
  if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
885
990
  changed = changed || warningComment.changed;
886
991
  } catch { /* preserve the primary error */ }
887
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
992
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
993
+ }
888
994
  }
889
995
 
890
- const summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
996
+ let summary = receipt.summary === 'pending'
997
+ ? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
998
+ : { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
891
999
  receipt = summary.receipt;
892
1000
  changed = changed || summary.changed;
893
1001
  if (summary.error) {
894
1002
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
895
- const warningComment = await syncPendingTaskComment({
1003
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
1004
+ }
1005
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
1006
+ if (receipt.taskComment === 'pending') {
1007
+ const finalComment = await syncPendingTaskComment({
896
1008
  repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
897
1009
  });
898
- receipt = warningComment.receipt;
899
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
900
- changed = changed || warningComment.changed;
901
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
902
- }
903
-
904
- let postSummaryVerification: TaskFinalizationStep | null = null;
905
- if (receipt.postSummaryVerification === 'skipped') {
906
- postSummaryVerification = { status: 'skipped', changed: false, error: null };
907
- } else if (receipt.postSummaryVerification !== 'pending') {
908
- const replay = await commentSync(taskId, {
909
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
910
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
911
- });
912
- postSummaryVerification = commentStep(replay);
913
- if (replay.status !== 'applied' && replay.status !== 'no-op') {
914
- const error = postSummaryVerification.error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary replay verification failed', retryable: true };
915
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
916
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
917
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
918
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
919
- }
920
- } else {
921
- try {
922
- const summaryVerification = await commentSync(taskId, {
923
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
924
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
925
- });
926
- if (summaryVerification.status !== 'applied' && summaryVerification.status !== 'no-op') {
927
- const error = commentStep(summaryVerification).error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary verification failed', retryable: true };
928
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
929
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
930
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
931
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
932
- receipt = warningComment.receipt;
933
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
934
- changed = changed || warningComment.changed;
935
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: commentStep(summaryVerification) }, changed, error);
936
- }
937
- const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
938
- postSummaryVerification = verificationStep(result);
939
- if (result.status === 'pass') {
940
- const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'post-summary-verification' && warning.status === 'open');
941
- const warnings = resolveStepWarnings(receipt, 'post-summary-verification');
942
- receipt = updateReceipt(repoRoot, receipt, {
943
- postSummaryVerification: 'done', taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
944
- warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done', warnings, lastError: null
945
- });
946
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
947
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
948
- receipt = warningComment.receipt;
949
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
950
- changed = changed || warningComment.changed;
951
- } else {
952
- const error = postSummaryVerification.error ?? { code: 'VERIFY_FAILED', message: 'post-summary verification failed', retryable: true };
953
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
954
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
955
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
956
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
957
- receipt = warningComment.receipt;
958
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
959
- changed = changed || warningComment.changed;
960
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
961
- }
962
- } catch (error) {
963
- const detail = errorOf(error, 'POST_SUMMARY_VERIFY_FAILED', true);
964
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', detail), 'open');
965
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail });
966
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
967
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
968
- receipt = warningComment.receipt;
969
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
970
- changed = changed || warningComment.changed;
971
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
1010
+ receipt = finalComment.receipt;
1011
+ taskComment = finalComment.step;
1012
+ changed = changed || finalComment.changed;
1013
+ if (finalComment.error) {
1014
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, finalComment.error);
972
1015
  }
973
1016
  }
974
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
975
- if (receipt.summary !== 'pending' && receipt.postSummaryVerification !== 'pending' && receipt.warningProjection === 'done' && openWarnings(receipt).length === 0) {
976
- const resolved = resolveTaskRef(taskId, { repoRoot });
977
- if (resolved.ok && resolved.state === 'completed') clearDeliverySummary(resolved.taskDir);
1017
+ return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
1018
+ }
1019
+
1020
+ async function prepareTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
1021
+ if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
1022
+ return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
1023
+ }
1024
+ const repoRoot = path.resolve(options.repoRoot);
1025
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot });
1026
+ if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
1027
+ try {
1028
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
1029
+ } catch (error) {
1030
+ const detail = error instanceof TaskExecutionLockError
1031
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
1032
+ : errorOf(error, 'TASK_FINALIZATION_FAILED');
1033
+ return failed(resolved.taskId, detail);
978
1034
  }
979
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
980
1035
  }
981
1036
 
982
- async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
1037
+ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
983
1038
  if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
984
1039
  return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
985
1040
  }
@@ -987,7 +1042,38 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
987
1042
  const resolved = resolveTaskRef(request.taskRef, { repoRoot });
988
1043
  if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
989
1044
  try {
990
- return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
1045
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
1046
+ const receipt = readReceipt(repoRoot, resolved.taskId);
1047
+ if (!receipt) return failed(resolved.taskId, {
1048
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
1049
+ });
1050
+ if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
1051
+ return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
1052
+ }
1053
+ if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1054
+ return failed(resolved.taskId, {
1055
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
1056
+ }, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
1057
+ }
1058
+ const registry = inspectShortIdRegistry(repoRoot);
1059
+ if (registry.status !== 'valid') return failed(resolved.taskId, {
1060
+ code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
1061
+ message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
1062
+ retryable: false
1063
+ });
1064
+ const lifecycle = options.lifecycle ?? applyTaskLifecycle;
1065
+ const result = lifecycle(
1066
+ { taskRef: resolved.taskId, intent: 'complete', agent: request.agent },
1067
+ { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
1068
+ );
1069
+ const step = lifecycleStep(result);
1070
+ if (result.status !== 'applied' && result.status !== 'no-op') {
1071
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
1072
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
1073
+ }
1074
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
1075
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
1076
+ });
991
1077
  } catch (error) {
992
1078
  const detail = error instanceof TaskExecutionLockError
993
1079
  ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
@@ -996,9 +1082,17 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
996
1082
  }
997
1083
  }
998
1084
 
1085
+ async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
1086
+ const prepared = await prepareTaskFinalization(request, options);
1087
+ if (prepared.status !== 'prepared') return prepared;
1088
+ return commitPreparedTaskFinalization(request, options);
1089
+ }
1090
+
999
1091
  export {
1000
1092
  applyFinalizationReceiptMutation,
1093
+ commitPreparedTaskFinalization,
1001
1094
  applyTaskFinalization,
1095
+ prepareTaskFinalization,
1002
1096
  issueCapability as createFinalizationCapability,
1003
1097
  readTaskFinalizationReceipt,
1004
1098
  terminalResult