@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
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { syncPlatformComment } from '../platform/issue-comments.js';
5
5
  import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
6
- import { backfillCompletionComments } from '../platform/completion-backfill.js';
6
+ import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
7
7
  import { taskIssueIdentity } from '../platform/task-identities.js';
8
8
  import { parseTaskFrontmatter } from './frontmatter.js';
9
9
  import { applyTaskLifecycle } from './lifecycle.js';
@@ -221,6 +221,77 @@ function resolveStepWarnings(receipt, step) {
221
221
  ? { ...warning, status: 'resolved', resolvedAt: now() }
222
222
  : warning);
223
223
  }
224
+ function verificationWarnings(result) {
225
+ const warnings = [];
226
+ for (const invocation of result.invocations) {
227
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
228
+ for (const value of checks) {
229
+ if (!value || typeof value !== 'object' || Array.isArray(value))
230
+ continue;
231
+ const check = value;
232
+ const status = check.effectiveStatus ?? check.status;
233
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
234
+ if (!target || status === 'pass')
235
+ continue;
236
+ const blocked = status === 'blocked';
237
+ const message = [check.reason, check.message, check.action]
238
+ .filter((item) => typeof item === 'string' && item.length > 0)
239
+ .join(' - ');
240
+ warnings.push({
241
+ step: 'verification', target, code: blocked ? 'CHECK_BLOCKED' : 'CHECK_FAILED',
242
+ retryable: blocked, severity: 'ACTION_REQUIRED',
243
+ message: message || `verification ${String(status)}`
244
+ });
245
+ }
246
+ }
247
+ return mergeOperationWarnings(warnings);
248
+ }
249
+ function applyVerificationWarnings(receipt, result) {
250
+ if (result.status === 'pass')
251
+ return resolveStepWarnings(receipt, 'verification');
252
+ const observed = new Map();
253
+ for (const invocation of result.invocations) {
254
+ const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
255
+ for (const value of checks) {
256
+ if (!value || typeof value !== 'object' || Array.isArray(value))
257
+ continue;
258
+ const check = value;
259
+ const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
260
+ if (!target)
261
+ continue;
262
+ const status = check.effectiveStatus ?? check.status;
263
+ if (status === 'pass')
264
+ observed.set(target, 'pass');
265
+ }
266
+ }
267
+ for (const warning of verificationWarnings(result))
268
+ observed.set(warning.target, warning);
269
+ if (observed.size === 0) {
270
+ return replaceWarning(receipt, warningFromError('verification', verificationFailure(result)), 'open');
271
+ }
272
+ let warnings = [...receipt.warnings];
273
+ for (const [target, current] of observed) {
274
+ if (current === 'pass') {
275
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target && warning.status === 'open'
276
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
277
+ : warning);
278
+ continue;
279
+ }
280
+ warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target
281
+ && warning.status === 'open' && warning.code !== current.code
282
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
283
+ : warning);
284
+ const key = warningKey(current);
285
+ warnings = [...warnings.filter((warning) => warningKey(warning) !== key), warningRecord(current, 'open')];
286
+ }
287
+ return warnings;
288
+ }
289
+ function shouldRunBackfill(receipt) {
290
+ return receipt.lifecycle !== 'done'
291
+ || receipt.taskComment === 'pending'
292
+ || receipt.verification === 'pending'
293
+ || receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open');
294
+ }
224
295
  function issueCapability(receipt, scope) {
225
296
  return {
226
297
  receiptId: receipt.receiptId,
@@ -236,6 +307,21 @@ function capabilityError(code, message) {
236
307
  Object.assign(error, { code });
237
308
  return error;
238
309
  }
310
+ function hasPreparedTaskDocument(repoRoot, taskId) {
311
+ const resolved = resolveTaskRef(taskId, { repoRoot });
312
+ if (!resolved.ok)
313
+ return false;
314
+ if (resolved.state === 'completed')
315
+ return true;
316
+ if (resolved.state !== 'active')
317
+ return false;
318
+ try {
319
+ return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed';
320
+ }
321
+ catch {
322
+ return false;
323
+ }
324
+ }
239
325
  function mutationKeys(value) {
240
326
  return Object.keys(value).sort();
241
327
  }
@@ -305,9 +391,8 @@ function mutationPatch(current, mutation) {
305
391
  : { warningProjection: 'pending', lastError: mutation.error };
306
392
  }
307
393
  function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, consumed) {
308
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
309
- if (!resolved.ok || resolved.state !== 'completed') {
310
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
394
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
395
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
311
396
  }
312
397
  const current = readReceipt(repoRoot, receipt.taskId);
313
398
  if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
@@ -319,11 +404,10 @@ function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability
319
404
  return next;
320
405
  }
321
406
  function applyFinalizationReceiptMutation(repoRoot, receipt, capability, mutation) {
322
- const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
323
- if (!resolved.ok || resolved.state !== 'completed') {
324
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
407
+ if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
408
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
325
409
  }
326
- return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
410
+ return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
327
411
  }
328
412
  function warningKey(warning) {
329
413
  return `${warning.step}\0${warning.code}\0${warning.target}`;
@@ -334,7 +418,8 @@ function reconcileWarningProjection(repoRoot, taskId, receipt, consumed) {
334
418
  const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
335
419
  for (const warning of warnings) {
336
420
  try {
337
- const projected = projectFinalizationWarning(taskId, warning, { repoRoot });
421
+ const superseded = warning.status === 'resolved' && receipt.warnings.some((item) => item.status === 'open' && item.step === warning.step && item.target === warning.target && item.code !== warning.code);
422
+ const projected = projectFinalizationWarning(taskId, warning, { repoRoot, resolution: superseded ? 'superseded' : 'passed' });
338
423
  if (projected.status === 'failed') {
339
424
  const detail = {
340
425
  code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
@@ -433,8 +518,12 @@ async function syncPendingSummary(input) {
433
518
  let { receipt } = input;
434
519
  try {
435
520
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
436
- if (!resolved.ok || resolved.state !== 'completed')
437
- throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
521
+ if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId))
522
+ throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
523
+ if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
524
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
525
+ return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
526
+ }
438
527
  if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
439
528
  receipt = updateReceipt(input.repoRoot, receipt, {
440
529
  summary: 'skipped', lastError: null
@@ -509,7 +598,19 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
509
598
  error: hardError ? error : postLifecyclePending ? null : error
510
599
  };
511
600
  }
512
- async function applyUnderLock(request, taskId, options) {
601
+ function preparedResult(taskId, receipt, steps, changed) {
602
+ return {
603
+ status: 'prepared', changed, taskId,
604
+ backfill: steps.backfill ?? null,
605
+ lifecycle: steps.lifecycle ?? null,
606
+ taskComment: steps.taskComment ?? null,
607
+ verification: steps.verification ?? null,
608
+ summary: steps.summary ?? null,
609
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt),
610
+ result: 'prepared', warnings: openWarnings(receipt), error: null
611
+ };
612
+ }
613
+ async function prepareUnderLock(request, taskId, options) {
513
614
  const repoRoot = path.resolve(options.repoRoot);
514
615
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
515
616
  const backfill = options.backfill ?? backfillCompletionComments;
@@ -551,80 +652,87 @@ async function applyUnderLock(request, taskId, options) {
551
652
  }
552
653
  let changed = false;
553
654
  let backfillResult = null;
554
- try {
555
- const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
556
- backfillResult = commentStep(result);
557
- const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
558
- if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
559
- const detail = backfillResult.error ?? {
560
- code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
561
- };
655
+ let runBackfill = shouldRunBackfill(receipt);
656
+ if (!runBackfill && receipt.lifecycle === 'done') {
657
+ const eligibility = inspectCompletionBackfillEligibility(taskId, { cwd: repoRoot });
658
+ if (eligibility.status === 'failed')
659
+ return failed(taskId, eligibility.error, {
660
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
661
+ });
662
+ runBackfill = eligibility.eligible;
663
+ }
664
+ if (receipt.lifecycle === 'done' && runBackfill && receipt.summary === 'done') {
665
+ receipt = updateReceipt(repoRoot, receipt, { summary: 'pending' });
666
+ }
667
+ if (runBackfill)
668
+ try {
669
+ const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
670
+ backfillResult = commentStep(result);
671
+ const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
672
+ if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
673
+ const detail = backfillResult.error ?? {
674
+ code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
675
+ };
676
+ if (receipt.lifecycle !== 'done')
677
+ return failed(taskId, detail, {
678
+ backfill: backfillResult,
679
+ completedSteps: completedSteps(receipt),
680
+ pendingSteps: pendingSteps(receipt)
681
+ });
682
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
683
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
684
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
685
+ if (!detail.retryable)
686
+ return failed(taskId, detail, {
687
+ changed, backfill: backfillResult, completedSteps: completedSteps(receipt),
688
+ pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
689
+ });
690
+ return terminalResult(taskId, receipt, { backfill: backfillResult }, changed, detail);
691
+ }
692
+ changed = result.changed;
693
+ if (receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open')) {
694
+ const warnings = receipt.warnings.map((warning) => warning.step === 'backfill' && warning.status === 'open'
695
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
696
+ : warning);
697
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: null });
698
+ }
699
+ if (result.changed && receipt.taskComment !== 'pending') {
700
+ receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
701
+ }
702
+ }
703
+ catch (error) {
704
+ const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
705
+ if (receipt.lifecycle === 'done') {
706
+ const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
707
+ receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
708
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
709
+ return terminalResult(taskId, receipt, {
710
+ backfill: { status: 'blocked', changed: false, error: detail }
711
+ }, changed, detail);
712
+ }
562
713
  return failed(taskId, detail, {
563
- backfill: backfillResult,
714
+ backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
564
715
  completedSteps: completedSteps(receipt),
565
716
  pendingSteps: pendingSteps(receipt)
566
717
  });
567
718
  }
568
- changed = result.changed;
569
- if (result.changed && receipt.taskComment !== 'pending') {
570
- receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
571
- }
572
- }
573
- catch (error) {
574
- const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
575
- return failed(taskId, detail, {
576
- backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
577
- completedSteps: completedSteps(receipt),
578
- pendingSteps: pendingSteps(receipt)
579
- });
580
- }
719
+ else
720
+ backfillResult = { status: 'no-op', changed: false, error: null };
581
721
  let lifecycleResult = null;
582
722
  try {
583
723
  const resolved = resolveTaskRef(taskId, { repoRoot });
584
- const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
585
- if (registry && registry.status !== 'valid') {
586
- const detail = {
587
- code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
588
- message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
589
- retryable: false
590
- };
591
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
592
- return terminalResult(taskId, receipt, {
593
- backfill: backfillResult,
594
- lifecycle: { status: 'failed', changed: false, error: detail },
595
- taskComment: null,
596
- verification: null
597
- }, changed, detail);
598
- }
599
- const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
600
- if (shortIds?.has(taskId)) {
601
- const detail = {
602
- code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
603
- message: 'completed task still has an active short-id registry entry',
604
- retryable: false
605
- };
606
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
607
- return terminalResult(taskId, receipt, {
608
- backfill: backfillResult,
609
- lifecycle: { status: 'failed', changed: false, error: detail },
610
- taskComment: null,
611
- verification: null
612
- }, changed, detail);
613
- }
614
- const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
615
- if (lifecycleDone) {
724
+ if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
616
725
  lifecycleResult = { status: 'no-op', changed: false, error: null };
617
726
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
618
727
  }
619
728
  else {
620
- const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
729
+ const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
621
730
  lifecycleResult = lifecycleStep(result);
622
731
  if (result.status !== 'applied' && result.status !== 'no-op') {
623
732
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
624
733
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
625
734
  }
626
735
  changed = changed || result.changed;
627
- receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
628
736
  }
629
737
  }
630
738
  catch (error) {
@@ -637,27 +745,26 @@ async function applyUnderLock(request, taskId, options) {
637
745
  }
638
746
  let taskComment = null;
639
747
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
640
- const initialComment = await syncPendingTaskComment({
641
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
642
- });
643
- receipt = initialComment.receipt;
644
- taskComment = initialComment.step;
645
- changed = changed || initialComment.changed;
646
- if (initialComment.error) {
647
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
648
- }
649
748
  let verification = null;
650
- try {
651
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
652
- const verificationPending = receipt.verification === 'pending';
653
- const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
654
- verification = verificationStep(result);
655
- if (result.status === 'pass') {
656
- if (verificationPending) {
657
- const capability = issueCapability(receipt, 'verification');
658
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
659
- scope: 'verification', operation: 'succeeded'
660
- }, consumedCapabilities);
749
+ if (receipt.verification !== 'pending') {
750
+ verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
751
+ }
752
+ else {
753
+ try {
754
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
755
+ const verificationState = resolveTaskRef(taskId, { repoRoot });
756
+ const verificationEvent = verificationState.ok && verificationState.state === 'completed'
757
+ ? 'complete-task.completed'
758
+ : 'complete-task.prepared';
759
+ const result = await verify({ taskRef: taskId, event: verificationEvent }, { repoRoot });
760
+ verification = verificationStep(result);
761
+ if (result.status === 'pass') {
762
+ const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
763
+ const warnings = applyVerificationWarnings(receipt, result);
764
+ receipt = updateReceipt(repoRoot, receipt, {
765
+ verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
766
+ warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
767
+ });
661
768
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
662
769
  const finalComment = await syncPendingTaskComment({
663
770
  repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
@@ -671,72 +778,58 @@ async function applyUnderLock(request, taskId, options) {
671
778
  }
672
779
  }
673
780
  else {
674
- receipt = updateReceipt(repoRoot, receipt, { lastError: null });
675
- }
676
- }
677
- else {
678
- const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
679
- if (verificationPending) {
680
- const capability = issueCapability(receipt, 'verification');
681
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
682
- scope: 'verification', operation: 'failed', error: detail
683
- }, consumedCapabilities);
684
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
685
- }
686
- else {
687
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
781
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
782
+ const warnings = applyVerificationWarnings(receipt, result);
688
783
  receipt = updateReceipt(repoRoot, receipt, {
689
784
  verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
690
785
  });
691
786
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
692
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
787
+ const warningComment = await syncPendingTaskComment({
788
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
789
+ });
693
790
  receipt = warningComment.receipt;
694
- if (warningComment.step.status !== 'no-op')
695
- taskComment = warningComment.step;
791
+ taskComment = warningComment.step;
696
792
  changed = changed || warningComment.changed;
793
+ if (warningComment.error) {
794
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
795
+ }
796
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
697
797
  }
698
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
699
798
  }
700
- }
701
- catch (error) {
702
- const detail = errorOf(error, 'VERIFY_FAILED', true);
703
- try {
704
- if (receipt.verification === 'pending') {
705
- const capability = issueCapability(receipt, 'verification');
706
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
707
- scope: 'verification', operation: 'failed', error: detail
708
- }, consumedCapabilities);
709
- }
710
- else {
799
+ catch (error) {
800
+ const detail = errorOf(error, 'VERIFY_FAILED', true);
801
+ try {
711
802
  const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
712
803
  receipt = updateReceipt(repoRoot, receipt, {
713
804
  verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
714
805
  });
806
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
715
807
  }
716
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
717
- }
718
- catch (writeError) {
719
- const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
720
- const hardError = {
721
- code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
722
- message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
723
- retryable: true
724
- };
725
- return terminalResult(taskId, receipt, {
726
- backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
727
- }, changed, hardError);
728
- }
729
- try {
730
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
731
- receipt = warningComment.receipt;
732
- if (warningComment.step.status !== 'no-op')
733
- taskComment = warningComment.step;
734
- changed = changed || warningComment.changed;
808
+ catch (writeError) {
809
+ const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
810
+ const hardError = {
811
+ code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
812
+ message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
813
+ retryable: true
814
+ };
815
+ return terminalResult(taskId, receipt, {
816
+ backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
817
+ }, changed, hardError);
818
+ }
819
+ try {
820
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
821
+ receipt = warningComment.receipt;
822
+ if (warningComment.step.status !== 'no-op')
823
+ taskComment = warningComment.step;
824
+ changed = changed || warningComment.changed;
825
+ }
826
+ catch { /* preserve the primary error */ }
827
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
735
828
  }
736
- catch { /* preserve the primary error */ }
737
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
738
829
  }
739
- let summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
830
+ let summary = receipt.summary === 'pending'
831
+ ? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
832
+ : { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
740
833
  receipt = summary.receipt;
741
834
  changed = changed || summary.changed;
742
835
  if (summary.error) {
@@ -744,9 +837,20 @@ async function applyUnderLock(request, taskId, options) {
744
837
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
745
838
  }
746
839
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
747
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
840
+ if (receipt.taskComment === 'pending') {
841
+ const finalComment = await syncPendingTaskComment({
842
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
843
+ });
844
+ receipt = finalComment.receipt;
845
+ taskComment = finalComment.step;
846
+ changed = changed || finalComment.changed;
847
+ if (finalComment.error) {
848
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, finalComment.error);
849
+ }
850
+ }
851
+ return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
748
852
  }
749
- async function applyTaskFinalization(request, options) {
853
+ async function prepareTaskFinalization(request, options) {
750
854
  if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
751
855
  return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
752
856
  }
@@ -755,7 +859,7 @@ async function applyTaskFinalization(request, options) {
755
859
  if (!resolved.ok)
756
860
  return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
757
861
  try {
758
- return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
862
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
759
863
  }
760
864
  catch (error) {
761
865
  const detail = error instanceof TaskExecutionLockError
@@ -764,5 +868,59 @@ async function applyTaskFinalization(request, options) {
764
868
  return failed(resolved.taskId, detail);
765
869
  }
766
870
  }
767
- export { applyFinalizationReceiptMutation, applyTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
871
+ async function commitPreparedTaskFinalization(request, options) {
872
+ if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
873
+ return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
874
+ }
875
+ const repoRoot = path.resolve(options.repoRoot);
876
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot });
877
+ if (!resolved.ok)
878
+ return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
879
+ try {
880
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
881
+ const receipt = readReceipt(repoRoot, resolved.taskId);
882
+ if (!receipt)
883
+ return failed(resolved.taskId, {
884
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
885
+ });
886
+ if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
887
+ return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
888
+ }
889
+ if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
890
+ return failed(resolved.taskId, {
891
+ code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
892
+ }, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
893
+ }
894
+ const registry = inspectShortIdRegistry(repoRoot);
895
+ if (registry.status !== 'valid')
896
+ return failed(resolved.taskId, {
897
+ code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
898
+ message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
899
+ retryable: false
900
+ });
901
+ const lifecycle = options.lifecycle ?? applyTaskLifecycle;
902
+ const result = lifecycle({ taskRef: resolved.taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
903
+ const step = lifecycleStep(result);
904
+ if (result.status !== 'applied' && result.status !== 'no-op') {
905
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
906
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
907
+ }
908
+ const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
909
+ return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
910
+ });
911
+ }
912
+ catch (error) {
913
+ const detail = error instanceof TaskExecutionLockError
914
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
915
+ : errorOf(error, 'TASK_FINALIZATION_FAILED');
916
+ return failed(resolved.taskId, detail);
917
+ }
918
+ }
919
+ async function applyTaskFinalization(request, options) {
920
+ const prepared = await prepareTaskFinalization(request, options);
921
+ if (prepared.status !== 'prepared')
922
+ return prepared;
923
+ return commitPreparedTaskFinalization(request, options);
924
+ }
925
+ export { applyFinalizationReceiptMutation, commitPreparedTaskFinalization, applyTaskFinalization, prepareTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
768
926
  //# sourceMappingURL=finalization.js.map