@fitlab-ai/agent-infra 0.11.3 → 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 (102) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -1
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-finalization.js +41 -1
  7. package/dist/lib/internal/task-ledger.js +13 -5
  8. package/dist/lib/internal/task-operation-registry.js +1 -0
  9. package/dist/lib/platform/completion-backfill.js +63 -17
  10. package/dist/lib/sandbox/control/server.js +26 -143
  11. package/dist/lib/sandbox/task-cleanup.js +13 -20
  12. package/dist/lib/task/artifact-command.js +11 -4
  13. package/dist/lib/task/artifact-lifecycle.js +217 -85
  14. package/dist/lib/task/artifact-operations.js +1 -26
  15. package/dist/lib/task/artifact-receipts.js +4 -1
  16. package/dist/lib/task/artifact-schema.js +1 -0
  17. package/dist/lib/task/artifact-selection.js +25 -0
  18. package/dist/lib/task/capabilities.js +158 -39
  19. package/dist/lib/task/commands/status.js +9 -2
  20. package/dist/lib/task/control-authority.js +3 -2
  21. package/dist/lib/task/decision-intents.js +12 -0
  22. package/dist/lib/task/delivery.js +1 -43
  23. package/dist/lib/task/events.js +80 -64
  24. package/dist/lib/task/finalization.js +301 -143
  25. package/dist/lib/task/ledger-intents.js +157 -15
  26. package/dist/lib/task/lifecycle-path.js +75 -0
  27. package/dist/lib/task/lifecycle.js +10 -0
  28. package/dist/lib/task/local-artifact-finalization.js +1 -20
  29. package/dist/lib/task/orchestration.js +17 -7
  30. package/dist/lib/task/review-finalization.js +3 -16
  31. package/dist/lib/task/rework-intent.js +70 -4
  32. package/dist/lib/task/verification-engine.js +20 -30
  33. package/dist/lib/task/verification.js +1 -0
  34. package/dist/lib/task/workflow-warning-intents.js +21 -6
  35. package/dist/lib/task/workspace-snapshot.js +7 -2
  36. package/dist/lifecycle-build-manifest.json +3 -3
  37. package/lib/internal/cli-route-inventory.ts +1 -1
  38. package/lib/internal/task-artifact.ts +1 -1
  39. package/lib/internal/task-finalization.ts +40 -1
  40. package/lib/internal/task-ledger.ts +12 -5
  41. package/lib/internal/task-operation-registry.ts +1 -0
  42. package/lib/platform/completion-backfill.ts +78 -16
  43. package/lib/sandbox/control/server.ts +21 -146
  44. package/lib/sandbox/task-cleanup.ts +12 -20
  45. package/lib/task/artifact-command.ts +14 -4
  46. package/lib/task/artifact-lifecycle.ts +236 -85
  47. package/lib/task/artifact-operations.ts +1 -26
  48. package/lib/task/artifact-receipts.ts +4 -1
  49. package/lib/task/artifact-schema.ts +1 -0
  50. package/lib/task/artifact-selection.ts +53 -0
  51. package/lib/task/capabilities.ts +154 -40
  52. package/lib/task/commands/status.ts +11 -2
  53. package/lib/task/control-authority.ts +3 -1
  54. package/lib/task/decision-intents.ts +9 -0
  55. package/lib/task/delivery.ts +1 -40
  56. package/lib/task/events.ts +82 -64
  57. package/lib/task/finalization.ts +269 -111
  58. package/lib/task/ledger-intents.ts +155 -15
  59. package/lib/task/lifecycle-path.ts +98 -0
  60. package/lib/task/lifecycle.ts +13 -0
  61. package/lib/task/local-artifact-finalization.ts +2 -25
  62. package/lib/task/orchestration.ts +26 -8
  63. package/lib/task/review-finalization.ts +3 -16
  64. package/lib/task/rework-intent.ts +75 -5
  65. package/lib/task/verification-engine.ts +19 -30
  66. package/lib/task/verification.ts +2 -1
  67. package/lib/task/workflow-warning-intents.ts +18 -6
  68. package/lib/task/workspace-snapshot.ts +3 -2
  69. package/package.json +1 -1
  70. package/templates/.agents/rules/review-handshake.en.md +4 -4
  71. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  72. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  73. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  74. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  75. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  76. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  77. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  78. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  79. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  82. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  83. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  84. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  85. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  86. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  87. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  88. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  89. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  90. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  91. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  92. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  93. package/templates/.agents/templates/task.en.md +2 -2
  94. package/templates/.agents/templates/task.zh-CN.md +2 -2
  95. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  96. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  97. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  98. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  99. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  100. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  101. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  102. package/lib/sandbox/control/finalization-response.ts +0 -35
@@ -4,7 +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 } from '../platform/completion-backfill.ts';
7
+ import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.ts';
8
8
  import type { PlatformResult } from '../platform/types.ts';
9
9
  import { taskIssueIdentity } from '../platform/task-identities.ts';
10
10
  import { parseTaskFrontmatter } from './frontmatter.ts';
@@ -101,7 +101,7 @@ 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
107
  backfill: TaskFinalizationStep | null;
@@ -111,7 +111,7 @@ type TaskFinalizationResult = Readonly<{
111
111
  summary: 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
  }>;
@@ -314,7 +314,7 @@ function verificationFailure(result: TaskVerificationResult): FinalizationError
314
314
  }
315
315
 
316
316
  function warningFromError(
317
- step: FinalizationStep,
317
+ step: string,
318
318
  error: FinalizationError,
319
319
  severity: OperationWarningSeverity = 'ACTION_REQUIRED'
320
320
  ): OperationWarning {
@@ -357,6 +357,76 @@ function resolveStepWarnings(receipt: TaskFinalizationReceipt, step: Finalizatio
357
357
  : warning);
358
358
  }
359
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
+
360
430
  function issueCapability(receipt: TaskFinalizationReceipt, scope: Exclude<FinalizationStep, 'lifecycle'> | 'warning-projection'): FinalizationCapability {
361
431
  return {
362
432
  receiptId: receipt.receiptId,
@@ -374,6 +444,15 @@ function capabilityError(code: 'FINALIZATION_CAPABILITY_STALE' | 'FINALIZATION_S
374
444
  return error;
375
445
  }
376
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
+
377
456
  function mutationKeys(value: FinalizationMutation): string[] {
378
457
  return Object.keys(value).sort();
379
458
  }
@@ -451,9 +530,8 @@ function applyFinalizationReceiptMutationUnderLock(
451
530
  mutation: FinalizationMutation,
452
531
  consumed: Set<string>
453
532
  ): TaskFinalizationReceipt {
454
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
455
- if (!resolved.ok || resolved.state !== 'completed') {
456
- 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');
457
535
  }
458
536
  const current = readReceipt(repoRoot, receipt.taskId);
459
537
  if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
@@ -471,11 +549,10 @@ function applyFinalizationReceiptMutation(
471
549
  capability: FinalizationCapability,
472
550
  mutation: FinalizationMutation
473
551
  ): TaskFinalizationReceipt {
474
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
475
- if (!resolved.ok || resolved.state !== 'completed') {
476
- 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');
477
554
  }
478
- return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.receipt-mutation', () =>
555
+ return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () =>
479
556
  applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set<string>())
480
557
  );
481
558
  }
@@ -489,7 +566,10 @@ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: T
489
566
  const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
490
567
  for (const warning of warnings) {
491
568
  try {
492
- 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' });
493
573
  if (projected.status === 'failed') {
494
574
  const detail: FinalizationError = {
495
575
  code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
@@ -595,7 +675,11 @@ async function syncPendingSummary(input: {
595
675
  let { receipt } = input;
596
676
  try {
597
677
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
598
- 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
+ }
599
683
  if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
600
684
  receipt = updateReceipt(input.repoRoot, receipt, {
601
685
  summary: 'skipped', lastError: null
@@ -676,7 +760,25 @@ function terminalResult(
676
760
  };
677
761
  }
678
762
 
679
- 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(
680
782
  request: TaskFinalizationRequest,
681
783
  taskId: string,
682
784
  options: TaskFinalizationOptions
@@ -725,7 +827,18 @@ async function applyUnderLock(
725
827
 
726
828
  let changed = false;
727
829
  let backfillResult: TaskFinalizationStep | null = null;
728
- try {
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 {
729
842
  const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
730
843
  backfillResult = commentStep(result);
731
844
  const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
@@ -733,66 +846,57 @@ async function applyUnderLock(
733
846
  const detail = backfillResult.error ?? {
734
847
  code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
735
848
  };
736
- return failed(taskId, detail, {
849
+ if (receipt.lifecycle !== 'done') return failed(taskId, detail, {
737
850
  backfill: backfillResult,
738
851
  completedSteps: completedSteps(receipt),
739
852
  pendingSteps: pendingSteps(receipt)
740
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);
741
862
  }
742
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
+ }
743
870
  if (result.changed && receipt.taskComment !== 'pending') {
744
871
  receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
745
872
  }
746
873
  } catch (error) {
747
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);
879
+ return terminalResult(taskId, receipt, {
880
+ backfill: { status: 'blocked', changed: false, error: detail }
881
+ }, changed, detail);
882
+ }
748
883
  return failed(taskId, detail, {
749
884
  backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
750
885
  completedSteps: completedSteps(receipt),
751
886
  pendingSteps: pendingSteps(receipt)
752
887
  });
753
- }
888
+ } else backfillResult = { status: 'no-op', changed: false, error: null };
754
889
 
755
890
  let lifecycleResult: TaskFinalizationStep | null = null;
756
891
  try {
757
892
  const resolved = resolveTaskRef(taskId, { repoRoot });
758
- const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
759
- if (registry && registry.status !== 'valid') {
760
- const detail: FinalizationError = {
761
- code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
762
- message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
763
- retryable: false
764
- };
765
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
766
- return terminalResult(taskId, receipt, {
767
- backfill: backfillResult,
768
- lifecycle: { status: 'failed', changed: false, error: detail },
769
- taskComment: null,
770
- verification: null
771
- }, changed, detail);
772
- }
773
- const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
774
- if (shortIds?.has(taskId)) {
775
- const detail: FinalizationError = {
776
- code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
777
- message: 'completed task still has an active short-id registry entry',
778
- retryable: false
779
- };
780
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
781
- return terminalResult(taskId, receipt, {
782
- backfill: backfillResult,
783
- lifecycle: { status: 'failed', changed: false, error: detail },
784
- taskComment: null,
785
- verification: null
786
- }, changed, detail);
787
- }
788
- const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
789
- if (lifecycleDone) {
893
+ if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
790
894
  lifecycleResult = { status: 'no-op', changed: false, error: null };
791
895
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
792
896
  } else {
793
897
  const result = lifecycle(
794
898
  { taskRef: taskId, intent: 'complete', agent: request.agent },
795
- { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
899
+ { repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
796
900
  );
797
901
  lifecycleResult = lifecycleStep(result);
798
902
  if (result.status !== 'applied' && result.status !== 'no-op') {
@@ -800,7 +904,6 @@ async function applyUnderLock(
800
904
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
801
905
  }
802
906
  changed = changed || result.changed;
803
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
804
907
  }
805
908
  } catch (error) {
806
909
  const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
@@ -810,79 +913,64 @@ async function applyUnderLock(
810
913
 
811
914
  let taskComment: TaskFinalizationStep | null = null;
812
915
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
813
- const initialComment = await syncPendingTaskComment({
814
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
815
- });
816
- receipt = initialComment.receipt;
817
- taskComment = initialComment.step;
818
- changed = changed || initialComment.changed;
819
- if (initialComment.error) {
820
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
821
- }
822
916
 
823
917
  let verification: TaskFinalizationStep | null = null;
824
- try {
918
+ if (receipt.verification !== 'pending') {
919
+ verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
920
+ } else {
921
+ try {
825
922
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
826
- 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';
827
927
  const result = await verify(
828
- { taskRef: taskId, event: 'complete-task.completed' },
928
+ { taskRef: taskId, event: verificationEvent },
829
929
  { repoRoot }
830
930
  );
831
931
  verification = verificationStep(result);
832
932
  if (result.status === 'pass') {
833
- if (verificationPending) {
834
- const capability = issueCapability(receipt, 'verification');
835
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
836
- scope: 'verification', operation: 'succeeded'
837
- }, consumedCapabilities);
838
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
839
- const finalComment = await syncPendingTaskComment({
840
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
841
- });
842
- receipt = finalComment.receipt;
843
- if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
844
- changed = changed || finalComment.changed;
845
- if (finalComment.error) {
846
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
847
- }
848
- } else {
849
- 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);
850
948
  }
851
949
  } else {
852
950
  const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
853
- if (verificationPending) {
854
- const capability = issueCapability(receipt, 'verification');
855
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
856
- scope: 'verification', operation: 'failed', error: detail
857
- }, consumedCapabilities);
858
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
859
- } else {
860
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
861
- receipt = updateReceipt(repoRoot, receipt, {
862
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
863
- });
864
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
865
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
866
- receipt = warningComment.receipt;
867
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
868
- 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);
869
964
  }
870
965
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
871
966
  }
872
967
  } catch (error) {
873
968
  const detail = errorOf(error, 'VERIFY_FAILED', true);
874
969
  try {
875
- if (receipt.verification === 'pending') {
876
- const capability = issueCapability(receipt, 'verification');
877
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
878
- scope: 'verification', operation: 'failed', error: detail
879
- }, consumedCapabilities);
880
- } else {
881
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
882
- receipt = updateReceipt(repoRoot, receipt, {
883
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
884
- });
885
- }
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
+ });
886
974
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
887
975
  } catch (writeError) {
888
976
  const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
@@ -902,9 +990,12 @@ async function applyUnderLock(
902
990
  changed = changed || warningComment.changed;
903
991
  } catch { /* preserve the primary error */ }
904
992
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
993
+ }
905
994
  }
906
995
 
907
- let 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 };
908
999
  receipt = summary.receipt;
909
1000
  changed = changed || summary.changed;
910
1001
  if (summary.error) {
@@ -912,10 +1003,21 @@ async function applyUnderLock(
912
1003
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
913
1004
  }
914
1005
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
915
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
1006
+ if (receipt.taskComment === 'pending') {
1007
+ const finalComment = await syncPendingTaskComment({
1008
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
1009
+ });
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);
1015
+ }
1016
+ }
1017
+ return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
916
1018
  }
917
1019
 
918
- async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
1020
+ async function prepareTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
919
1021
  if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
920
1022
  return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
921
1023
  }
@@ -923,7 +1025,7 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
923
1025
  const resolved = resolveTaskRef(request.taskRef, { repoRoot });
924
1026
  if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
925
1027
  try {
926
- return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
1028
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
927
1029
  } catch (error) {
928
1030
  const detail = error instanceof TaskExecutionLockError
929
1031
  ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
@@ -932,9 +1034,65 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
932
1034
  }
933
1035
  }
934
1036
 
1037
+ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
1038
+ if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
1039
+ return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
1040
+ }
1041
+ const repoRoot = path.resolve(options.repoRoot);
1042
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot });
1043
+ if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
1044
+ try {
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
+ });
1077
+ } catch (error) {
1078
+ const detail = error instanceof TaskExecutionLockError
1079
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
1080
+ : errorOf(error, 'TASK_FINALIZATION_FAILED');
1081
+ return failed(resolved.taskId, detail);
1082
+ }
1083
+ }
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
+
935
1091
  export {
936
1092
  applyFinalizationReceiptMutation,
1093
+ commitPreparedTaskFinalization,
937
1094
  applyTaskFinalization,
1095
+ prepareTaskFinalization,
938
1096
  issueCapability as createFinalizationCapability,
939
1097
  readTaskFinalizationReceipt,
940
1098
  terminalResult