@fitlab-ai/agent-infra 0.11.7 → 0.11.9

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 (128) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +8 -2
  4. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  5. package/dist/lib/agent-clients/config.js +11 -3
  6. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  7. package/dist/lib/agent-clients/next-steps.js +1 -1
  8. package/dist/lib/agent-clients/reconcile.js +3 -3
  9. package/dist/lib/agent-clients/schema.js +18 -10
  10. package/dist/lib/defaults.json +6 -3
  11. package/dist/lib/init.js +2 -2
  12. package/dist/lib/internal/agent-client.js +3 -3
  13. package/dist/lib/internal/cli-route-inventory.js +3 -0
  14. package/dist/lib/internal/platform-pr-review.js +20 -3
  15. package/dist/lib/internal/task-operation-registry.js +3 -1
  16. package/dist/lib/internal/task-platform-recovery.js +56 -0
  17. package/dist/lib/merge.js +25 -5
  18. package/dist/lib/platform/completion-backfill.js +2 -1
  19. package/dist/lib/platform/in-label-sync.js +12 -1
  20. package/dist/lib/platform/issue-comments.js +58 -4
  21. package/dist/lib/platform/issues.js +105 -5
  22. package/dist/lib/platform/operation-coordinator.js +74 -0
  23. package/dist/lib/platform/pr-review.js +107 -1
  24. package/dist/lib/platform/pr-summary.js +79 -57
  25. package/dist/lib/platform/pull-requests.js +96 -4
  26. package/dist/lib/sandbox/commands/create.js +6 -3
  27. package/dist/lib/sandbox/commands/ls.js +4 -3
  28. package/dist/lib/sandbox/commands/rm.js +1 -1
  29. package/dist/lib/sandbox/config.js +22 -4
  30. package/dist/lib/sandbox/git-metadata.js +50 -0
  31. package/dist/lib/sandbox/index.js +1 -1
  32. package/dist/lib/sandbox/mounts.js +12 -0
  33. package/dist/lib/sandbox/recovery.js +12 -22
  34. package/dist/lib/sandbox/removal.js +58 -20
  35. package/dist/lib/sandbox/tools.js +31 -7
  36. package/dist/lib/task/activity-intent.js +4 -3
  37. package/dist/lib/task/create-service.js +5 -3
  38. package/dist/lib/task/create.js +22 -5
  39. package/dist/lib/task/finalization-artifacts.js +4 -1
  40. package/dist/lib/task/finalization.js +243 -113
  41. package/dist/lib/task/lifecycle.js +38 -2
  42. package/dist/lib/task/platform-operation-journal.js +152 -0
  43. package/dist/lib/task/platform-operation-recovery.js +459 -0
  44. package/dist/lib/task/verification.js +0 -1
  45. package/dist/lib/task/workflow-warning-intents.js +7 -1
  46. package/dist/lib/update.js +2 -2
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapters/codex.ts +1 -1
  49. package/lib/agent-clients/config.ts +9 -3
  50. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  51. package/lib/agent-clients/next-steps.ts +3 -3
  52. package/lib/agent-clients/reconcile.ts +3 -3
  53. package/lib/agent-clients/schema.ts +18 -10
  54. package/lib/defaults.json +6 -3
  55. package/lib/init.ts +2 -2
  56. package/lib/internal/agent-client.ts +3 -3
  57. package/lib/internal/cli-route-inventory.ts +2 -0
  58. package/lib/internal/platform-pr-review.ts +14 -3
  59. package/lib/internal/task-operation-registry.ts +3 -1
  60. package/lib/internal/task-platform-recovery.ts +53 -0
  61. package/lib/merge.ts +26 -6
  62. package/lib/platform/completion-backfill.ts +2 -1
  63. package/lib/platform/in-label-sync.ts +12 -0
  64. package/lib/platform/issue-comments.ts +59 -3
  65. package/lib/platform/issues.ts +103 -5
  66. package/lib/platform/operation-coordinator.ts +96 -0
  67. package/lib/platform/pr-review.ts +113 -1
  68. package/lib/platform/pr-summary.ts +22 -1
  69. package/lib/platform/pull-requests.ts +105 -5
  70. package/lib/sandbox/commands/create.ts +7 -8
  71. package/lib/sandbox/commands/ls.ts +4 -3
  72. package/lib/sandbox/commands/rm.ts +1 -1
  73. package/lib/sandbox/config.ts +23 -5
  74. package/lib/sandbox/git-metadata.ts +66 -0
  75. package/lib/sandbox/index.ts +1 -1
  76. package/lib/sandbox/mounts.ts +14 -0
  77. package/lib/sandbox/recovery.ts +13 -25
  78. package/lib/sandbox/removal.ts +57 -19
  79. package/lib/sandbox/tool-types.ts +2 -1
  80. package/lib/sandbox/tools.ts +38 -7
  81. package/lib/task/activity-intent.ts +4 -3
  82. package/lib/task/create-service.ts +5 -3
  83. package/lib/task/create.ts +17 -5
  84. package/lib/task/finalization-artifacts.ts +4 -1
  85. package/lib/task/finalization.ts +242 -106
  86. package/lib/task/lifecycle.ts +35 -3
  87. package/lib/task/platform-operation-journal.ts +230 -0
  88. package/lib/task/platform-operation-recovery.ts +435 -0
  89. package/lib/task/verification.ts +1 -2
  90. package/lib/task/workflow-warning-intents.ts +9 -1
  91. package/lib/update.ts +2 -3
  92. package/package.json +1 -1
  93. package/templates/.agents/README.en.md +20 -66
  94. package/templates/.agents/README.zh-CN.md +19 -65
  95. package/templates/.agents/rules/README.en.md +1 -1
  96. package/templates/.agents/rules/README.zh-CN.md +1 -1
  97. package/templates/.agents/rules/next-step-output.en.md +1 -1
  98. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  99. package/templates/.agents/rules/task-management.en.md +12 -0
  100. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  101. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  102. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
  103. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  106. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  108. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  109. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  110. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  111. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  112. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  113. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  114. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  115. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  116. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  117. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  118. package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
  119. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
  120. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  121. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  122. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  123. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  124. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  125. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  126. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +21 -274
  127. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  128. package/lib/agent-clients/custom-tuis.ts +0 -149
@@ -5,8 +5,11 @@ import { syncPlatformComment } from '../platform/issue-comments.js';
5
5
  import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
6
6
  import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
7
7
  import { taskIssueIdentity } from '../platform/task-identities.js';
8
+ import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
9
+ import { resourceIdentityEquals } from '../platform/resource-identity.js';
8
10
  import { parseTaskFrontmatter } from './frontmatter.js';
9
- import { applyTaskLifecycle, inspectTaskLifecycleProgress } from './lifecycle.js';
11
+ import { recoverPlatformOperations } from './platform-operation-recovery.js';
12
+ import { applyTaskLifecycle, inspectTaskLifecycleProgress, previewTaskCompletion } from './lifecycle.js';
10
13
  import { resolveTaskRef } from './resolve-ref.js';
11
14
  import { inspectShortIdRegistry } from './short-id.js';
12
15
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
@@ -329,19 +332,16 @@ function capabilityError(code, message) {
329
332
  Object.assign(error, { code });
330
333
  return error;
331
334
  }
332
- function hasPreparedTaskDocument(repoRoot, taskId) {
335
+ function hasFinalizationTaskDocument(repoRoot, taskId) {
333
336
  const resolved = resolveTaskRef(taskId, { repoRoot });
334
- if (!resolved.ok)
335
- return false;
336
- if (resolved.state === 'completed')
337
- return true;
338
- if (resolved.state !== 'active')
337
+ if (!resolved.ok || (resolved.state !== 'active' && resolved.state !== 'completed'))
339
338
  return false;
340
339
  try {
341
- return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed';
340
+ const journal = JSON.parse(fs.readFileSync(path.join(resolved.taskDir, '.task-lifecycle.json'), 'utf8'));
341
+ return journal.taskId === taskId && journal.intent === 'complete';
342
342
  }
343
343
  catch {
344
- return false;
344
+ return resolved.state === 'completed';
345
345
  }
346
346
  }
347
347
  function mutationKeys(value) {
@@ -388,7 +388,14 @@ function mutationPatch(current, mutation) {
388
388
  if (mutation.scope === 'task-comment') {
389
389
  if (mutation.operation === 'succeeded') {
390
390
  const warnings = resolveStepWarnings(current, 'task-comment');
391
- return { taskComment: mutation.state, warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null };
391
+ const retryWithResolvedWarning = mutation.state === 'done'
392
+ && current.warnings.some((warning) => warning.step === 'task-comment' && warning.status === 'open');
393
+ return {
394
+ taskComment: retryWithResolvedWarning ? 'pending' : mutation.state,
395
+ warningProjection: warnings.length > 0 || retryWithResolvedWarning ? 'pending' : 'done',
396
+ warnings,
397
+ lastError: null
398
+ };
392
399
  }
393
400
  const warnings = replaceWarning(current, warningFromError('task-comment', mutation.error), 'open');
394
401
  return { taskComment: 'pending', warningProjection: 'pending', warnings, lastError: mutation.error };
@@ -413,8 +420,8 @@ function mutationPatch(current, mutation) {
413
420
  : { warningProjection: 'pending', lastError: mutation.error };
414
421
  }
415
422
  function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, consumed) {
416
- if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
417
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
423
+ if (!hasFinalizationTaskDocument(repoRoot, receipt.taskId)) {
424
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active task or a completed task');
418
425
  }
419
426
  const current = readReceipt(repoRoot, receipt.taskId);
420
427
  if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
@@ -426,8 +433,8 @@ function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability
426
433
  return next;
427
434
  }
428
435
  function applyFinalizationReceiptMutation(repoRoot, receipt, capability, mutation) {
429
- if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
430
- throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
436
+ if (!hasFinalizationTaskDocument(repoRoot, receipt.taskId)) {
437
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active task or a completed task');
431
438
  }
432
439
  return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
433
440
  }
@@ -483,7 +490,15 @@ async function syncPendingTaskComment(input) {
483
490
  };
484
491
  }
485
492
  try {
486
- const result = await commentSync(taskId, { kind: 'task', agent, cwd: repoRoot });
493
+ const result = await commentSync(taskId, {
494
+ kind: 'task', agent, cwd: repoRoot, dependency: 'required',
495
+ ...(input.projection ? {
496
+ taskProjection: {
497
+ content: input.projection.content,
498
+ sha256: createHash('sha256').update(input.projection.content).digest('hex')
499
+ }
500
+ } : {})
501
+ });
487
502
  const step = commentStep(result);
488
503
  if (result.status === 'applied' || result.status === 'no-op') {
489
504
  const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
@@ -492,6 +507,14 @@ async function syncPendingTaskComment(input) {
492
507
  scope: 'task-comment', operation: 'succeeded', state: skipped ? 'skipped' : 'done'
493
508
  }, consumedCapabilities);
494
509
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
510
+ if (!skipped && receipt.taskComment === 'pending') {
511
+ const retry = await syncPendingTaskComment({
512
+ ...input,
513
+ receipt,
514
+ projection: input.refreshProjection?.() ?? input.projection
515
+ });
516
+ return { ...retry, changed: result.changed || retry.changed };
517
+ }
495
518
  return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
496
519
  }
497
520
  const detail = step.error ?? { code: 'COMMENT_SYNC_FAILED', message: 'task comment synchronization failed', retryable: true };
@@ -540,8 +563,8 @@ async function syncPendingSummary(input) {
540
563
  let { receipt } = input;
541
564
  try {
542
565
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
543
- if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId))
544
- throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
566
+ if (!resolved.ok || (resolved.state !== 'active' && resolved.state !== 'completed'))
567
+ throw Object.assign(new Error('task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
545
568
  if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
546
569
  receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
547
570
  return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
@@ -555,7 +578,8 @@ async function syncPendingSummary(input) {
555
578
  const summary = readDeliverySummary(resolved.taskDir, input.taskId);
556
579
  const result = await input.commentSync(input.taskId, {
557
580
  kind: 'summary', agent: input.agent, body: summary.body, cwd: input.repoRoot,
558
- summaryAuthorization: { sha256: summary.sha256 }
581
+ summaryAuthorization: { sha256: summary.sha256 },
582
+ dependency: 'required'
559
583
  });
560
584
  const step = commentStep(result);
561
585
  if (result.status === 'applied' || result.status === 'no-op') {
@@ -565,7 +589,7 @@ async function syncPendingSummary(input) {
565
589
  receipt = updateReceipt(input.repoRoot, receipt, {
566
590
  summary: skipped ? 'skipped' : 'done',
567
591
  taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
568
- warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
592
+ warningProjection: warnings.some((warning) => warning.status === 'open') || resolvedWarning ? 'pending' : 'done',
569
593
  warnings,
570
594
  lastError: null
571
595
  });
@@ -595,9 +619,12 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
595
619
  const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
596
620
  const warnings = openWarnings(receipt);
597
621
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
598
- const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
622
+ const terminalVerificationPending = receipt.lifecycle === 'done' && receipt.verification === 'pending';
623
+ const resultError = error ?? (terminalVerificationPending ? steps.verification?.error : null) ?? null;
624
+ const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID'
625
+ || terminalVerificationPending;
599
626
  return {
600
- status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
627
+ status: hardError ? (resultError?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
601
628
  changed,
602
629
  taskId,
603
630
  backfill: steps.backfill ?? null,
@@ -608,7 +635,7 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
608
635
  completedSteps: completedSteps(receipt),
609
636
  pendingSteps: pending,
610
637
  result: hardError
611
- ? (error?.retryable ? 'blocked' : 'failed')
638
+ ? (resultError?.retryable ? 'blocked' : 'failed')
612
639
  : postLifecyclePending
613
640
  ? 'completed_with_warnings'
614
641
  : pending.length === 0
@@ -617,7 +644,7 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
617
644
  ? 'blocked'
618
645
  : 'failed',
619
646
  warnings,
620
- error: hardError ? error : postLifecyclePending ? null : error
647
+ error: hardError ? resultError : postLifecyclePending ? null : resultError
621
648
  };
622
649
  }
623
650
  function preparedResult(taskId, receipt, steps, changed) {
@@ -637,7 +664,8 @@ async function prepareUnderLock(request, taskId, options) {
637
664
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
638
665
  const backfill = options.backfill ?? backfillCompletionComments;
639
666
  const commentSync = options.commentSync ?? syncPlatformComment;
640
- const verify = options.verify ?? verifyTaskEvent;
667
+ const issueSync = options.issueSync ?? syncPlatformIssue;
668
+ const issueInspect = options.issueInspect ?? inspectPlatformIssue;
641
669
  const consumedCapabilities = new Set();
642
670
  let receipt;
643
671
  try {
@@ -679,6 +707,101 @@ async function prepareUnderLock(request, taskId, options) {
679
707
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_PREFLIGHT_FAILED', true));
680
708
  }
681
709
  }
710
+ if (preflightState.ok && preflightState.state === 'active') {
711
+ const recovery = await recoverPlatformOperations(taskId, 'all', { agent: request.agent, cwd: repoRoot });
712
+ if (recovery.status !== 'applied' && recovery.status !== 'no-op') {
713
+ return failed(taskId, recovery.error ?? {
714
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
715
+ message: 'Pending platform operations remain unresolved',
716
+ retryable: true
717
+ });
718
+ }
719
+ const taskContent = fs.readFileSync(preflightState.taskMdPath, 'utf8');
720
+ const taskFrontmatter = parseTaskFrontmatter(taskContent);
721
+ const issueIdentity = taskIssueIdentity(taskFrontmatter);
722
+ if (issueIdentity) {
723
+ const base = typeof taskFrontmatter.delivery_base_ref === 'string' ? taskFrontmatter.delivery_base_ref.trim() : '';
724
+ let synced;
725
+ try {
726
+ synced = await issueSync(taskId, {
727
+ agent: request.agent, cwd: repoRoot, requirements: true, issueType: true, fields: true,
728
+ ...(base ? { inLabels: 'from-diff', base } : {}), dependency: 'required'
729
+ });
730
+ }
731
+ catch (error) {
732
+ const detail = errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true);
733
+ return failed(taskId, detail);
734
+ }
735
+ const issueOperations = synced.operations;
736
+ const badOperation = issueOperations?.find((operation) => ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])].includes(operation.name)
737
+ && operation.status !== 'applied' && operation.status !== 'no-op'
738
+ && !(operation.name === 'requirements' && operation.status === 'skipped' && operation.reasonCode === 'NO_REQUIREMENTS_ANCHOR'));
739
+ if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
740
+ const error = synced.error ?? {
741
+ code: badOperation?.reasonCode || 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
742
+ message: `Required Issue metadata operation '${badOperation?.name ?? 'sync'}' was not confirmed`,
743
+ retryable: true
744
+ };
745
+ return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true));
746
+ }
747
+ let inspected;
748
+ try {
749
+ inspected = await issueInspect(taskId, { cwd: repoRoot });
750
+ }
751
+ catch (error) {
752
+ return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_INSPECT_FAILED', true));
753
+ }
754
+ if (inspected.status !== 'no-op' || inspected.error || !inspected.issue
755
+ || !resourceIdentityEquals(inspected.issue.identity, issueIdentity)) {
756
+ return failed(taskId, inspected.error ?? {
757
+ code: 'FINALIZATION_ISSUE_IDENTITY_UNCONFIRMED',
758
+ message: 'Required Issue metadata could not be reread under the bound Issue identity',
759
+ retryable: true
760
+ });
761
+ }
762
+ for (const name of ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])]) {
763
+ const operation = issueOperations?.find((candidate) => candidate.name === name);
764
+ if (!operation)
765
+ return failed(taskId, {
766
+ code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
767
+ message: `Required Issue metadata operation '${name}' was not reported`, retryable: true
768
+ });
769
+ const before = synced.issue;
770
+ const remote = inspected.issue;
771
+ if (name === 'requirements') {
772
+ const expected = typeof operation.value === 'string' ? operation.value : before?.body;
773
+ if (expected === undefined || remote.body !== expected)
774
+ return failed(taskId, {
775
+ code: 'FINALIZATION_ISSUE_REQUIREMENTS_UNCONFIRMED', message: 'Issue requirements did not match the verified target', retryable: true
776
+ });
777
+ }
778
+ else if (name === 'issue-type') {
779
+ const expected = typeof operation.value === 'string' ? operation.value : before?.issueType;
780
+ if (!expected || remote.issueType !== expected)
781
+ return failed(taskId, {
782
+ code: 'FINALIZATION_ISSUE_TYPE_UNCONFIRMED', message: 'Issue Type did not match the verified target', retryable: true
783
+ });
784
+ }
785
+ else if (name === 'labels:in') {
786
+ const expected = Array.isArray(operation.value) ? operation.value : before?.labels;
787
+ if (!expected || expected.filter((label) => label.startsWith('in:')).sort().join('\0')
788
+ !== remote.labels.filter((label) => label.startsWith('in:')).sort().join('\0'))
789
+ return failed(taskId, {
790
+ code: 'FINALIZATION_IN_LABELS_UNCONFIRMED', message: 'Issue in-labels did not match the verified target', retryable: true
791
+ });
792
+ }
793
+ else {
794
+ const expected = operation.value && typeof operation.value === 'object'
795
+ ? operation.value
796
+ : before?.fields;
797
+ if (!expected || Object.entries(expected).some(([field, value]) => remote.fields[field] !== value))
798
+ return failed(taskId, {
799
+ code: 'FINALIZATION_ISSUE_FIELDS_UNCONFIRMED', message: 'Pinned Issue fields did not match the verified target', retryable: true
800
+ });
801
+ }
802
+ }
803
+ }
804
+ }
682
805
  let changed = false;
683
806
  let backfillResult = null;
684
807
  let runBackfill = shouldRunBackfill(receipt);
@@ -748,9 +871,10 @@ async function prepareUnderLock(request, taskId, options) {
748
871
  else
749
872
  backfillResult = { status: 'no-op', changed: false, error: null };
750
873
  let lifecycleResult = null;
874
+ let projection;
751
875
  try {
752
876
  const resolved = resolveTaskRef(taskId, { repoRoot });
753
- if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
877
+ if (resolved.ok && resolved.state === 'completed') {
754
878
  lifecycleResult = { status: 'no-op', changed: false, error: null };
755
879
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
756
880
  }
@@ -762,6 +886,12 @@ async function prepareUnderLock(request, taskId, options) {
762
886
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
763
887
  }
764
888
  changed = changed || result.changed;
889
+ if (result.timestamp && result.agentInfraVersion) {
890
+ projection = previewTaskCompletion(taskId, request.agent, {
891
+ repoRoot,
892
+ metadataProvider: () => ({ timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion })
893
+ });
894
+ }
765
895
  }
766
896
  }
767
897
  catch (error) {
@@ -774,88 +904,15 @@ async function prepareUnderLock(request, taskId, options) {
774
904
  }
775
905
  let taskComment = null;
776
906
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
777
- let verification = null;
778
- if (receipt.verification !== 'pending') {
779
- verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
780
- }
781
- else {
782
- try {
783
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
784
- const verificationState = resolveTaskRef(taskId, { repoRoot });
785
- const verificationEvent = verificationState.ok && verificationState.state === 'completed'
786
- ? 'complete-task.completed'
787
- : 'complete-task.prepared';
788
- const result = await verify({ taskRef: taskId, event: verificationEvent }, { repoRoot });
789
- verification = verificationStep(result);
790
- if (result.status === 'pass') {
791
- const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
792
- const warnings = applyVerificationWarnings(receipt, result);
793
- receipt = updateReceipt(repoRoot, receipt, {
794
- verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
795
- warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
796
- });
797
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
798
- const finalComment = await syncPendingTaskComment({
799
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
800
- });
801
- receipt = finalComment.receipt;
802
- if (finalComment.step.status !== 'no-op')
803
- taskComment = finalComment.step;
804
- changed = changed || finalComment.changed;
805
- if (finalComment.error) {
806
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
807
- }
808
- }
809
- else {
810
- const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
811
- const warnings = applyVerificationWarnings(receipt, result);
812
- receipt = updateReceipt(repoRoot, receipt, {
813
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
814
- });
815
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
816
- const warningComment = await syncPendingTaskComment({
817
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
818
- });
819
- receipt = warningComment.receipt;
820
- taskComment = warningComment.step;
821
- changed = changed || warningComment.changed;
822
- if (warningComment.error) {
823
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
824
- }
825
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
826
- }
827
- }
828
- catch (error) {
829
- const detail = errorOf(error, 'VERIFY_FAILED', true);
830
- try {
831
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
832
- receipt = updateReceipt(repoRoot, receipt, {
833
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
834
- });
835
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
836
- }
837
- catch (writeError) {
838
- const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
839
- const hardError = {
840
- code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
841
- message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
842
- retryable: true
843
- };
844
- return terminalResult(taskId, receipt, {
845
- backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
846
- }, changed, hardError);
847
- }
848
- try {
849
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
850
- receipt = warningComment.receipt;
851
- if (warningComment.step.status !== 'no-op')
852
- taskComment = warningComment.step;
853
- changed = changed || warningComment.changed;
854
- }
855
- catch { /* preserve the primary error */ }
856
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
857
- }
858
- }
907
+ const currentProjection = () => projection
908
+ ? previewTaskCompletion(taskId, request.agent, {
909
+ repoRoot,
910
+ metadataProvider: () => ({ timestamp: projection.timestamp, agentInfraVersion: projection.agentInfraVersion })
911
+ })
912
+ : undefined;
913
+ const verification = receipt.verification === 'done'
914
+ ? { status: 'no-op', changed: false, error: null }
915
+ : { status: 'skipped', changed: false, error: null };
859
916
  let summary = receipt.summary === 'pending'
860
917
  ? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
861
918
  : { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
@@ -868,7 +925,7 @@ async function prepareUnderLock(request, taskId, options) {
868
925
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
869
926
  if (receipt.taskComment === 'pending') {
870
927
  const finalComment = await syncPendingTaskComment({
871
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
928
+ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
872
929
  });
873
930
  receipt = finalComment.receipt;
874
931
  taskComment = finalComment.step;
@@ -906,7 +963,7 @@ async function commitPreparedTaskFinalization(request, options) {
906
963
  if (!resolved.ok)
907
964
  return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
908
965
  try {
909
- return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
966
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', async () => {
910
967
  if (options.controlBinding && options.manifestPath) {
911
968
  try {
912
969
  const manifest = readSandboxControlManifest(options.manifestPath);
@@ -958,7 +1015,7 @@ async function commitPreparedTaskFinalization(request, options) {
958
1015
  return failed(resolved.taskId, {
959
1016
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
960
1017
  });
961
- if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
1018
+ if (receipt.lifecycle === 'done' && receipt.verification !== 'pending' && resolved.state === 'completed') {
962
1019
  return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
963
1020
  }
964
1021
  if (options.controlBinding) {
@@ -997,7 +1054,7 @@ async function commitPreparedTaskFinalization(request, options) {
997
1054
  }
998
1055
  }
999
1056
  }
1000
- if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1057
+ if (receipt.taskComment === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1001
1058
  return failed(resolved.taskId, {
1002
1059
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
1003
1060
  }, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
@@ -1009,6 +1066,7 @@ async function commitPreparedTaskFinalization(request, options) {
1009
1066
  message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
1010
1067
  retryable: false
1011
1068
  });
1069
+ receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
1012
1070
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
1013
1071
  const result = lifecycle({ taskRef: resolved.taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
1014
1072
  const step = lifecycleStep(result);
@@ -1016,8 +1074,80 @@ async function commitPreparedTaskFinalization(request, options) {
1016
1074
  const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
1017
1075
  return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
1018
1076
  }
1019
- const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
1020
- return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
1077
+ let verificationResult;
1078
+ try {
1079
+ verificationResult = await (options.verify ?? verifyTaskEvent)({ taskRef: resolved.taskId, event: 'complete-task.completed' }, { repoRoot });
1080
+ }
1081
+ catch (error) {
1082
+ const detail = errorOf(error, 'VERIFY_FAILED', true);
1083
+ const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
1084
+ let updated;
1085
+ try {
1086
+ updated = updateReceipt(repoRoot, receipt, {
1087
+ lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
1088
+ warnings, lastError: detail
1089
+ });
1090
+ }
1091
+ catch (persistCause) {
1092
+ const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
1093
+ persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
1094
+ persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
1095
+ return failed(resolved.taskId, persistError, {
1096
+ changed: result.changed,
1097
+ lifecycle: step,
1098
+ verification: { status: 'blocked', changed: false, error: persistError },
1099
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
1100
+ });
1101
+ }
1102
+ const verification = { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail };
1103
+ return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
1104
+ }
1105
+ const verification = verificationStep(verificationResult);
1106
+ if (verificationResult.status === 'pass') {
1107
+ const resolvedVerificationWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
1108
+ const warnings = applyVerificationWarnings(receipt, verificationResult);
1109
+ const hasOpenWarnings = warnings.some((warning) => warning.status === 'open');
1110
+ let updated;
1111
+ try {
1112
+ updated = updateReceipt(repoRoot, receipt, {
1113
+ lifecycle: 'done', verification: 'done',
1114
+ taskComment: resolvedVerificationWarning ? 'pending' : receipt.taskComment,
1115
+ warningProjection: hasOpenWarnings ? 'pending' : 'done', warnings, lastError: null
1116
+ });
1117
+ }
1118
+ catch (persistCause) {
1119
+ const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
1120
+ persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
1121
+ return failed(resolved.taskId, persistError, {
1122
+ changed: result.changed,
1123
+ lifecycle: step,
1124
+ verification,
1125
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
1126
+ });
1127
+ }
1128
+ return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed);
1129
+ }
1130
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
1131
+ const warnings = applyVerificationWarnings(receipt, verificationResult);
1132
+ let updated;
1133
+ try {
1134
+ updated = updateReceipt(repoRoot, receipt, {
1135
+ lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
1136
+ warnings, lastError: detail
1137
+ });
1138
+ }
1139
+ catch (persistCause) {
1140
+ const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
1141
+ persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
1142
+ persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
1143
+ return failed(resolved.taskId, persistError, {
1144
+ changed: result.changed,
1145
+ lifecycle: step,
1146
+ verification: { status: 'blocked', changed: false, error: persistError },
1147
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
1148
+ });
1149
+ }
1150
+ return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
1021
1151
  });
1022
1152
  }
1023
1153
  catch (error) {
@@ -4,7 +4,8 @@ import path from 'node:path';
4
4
  import { appendActivityEntry, locateActivityLog } from './activity-log.js';
5
5
  import { artifactFamilyCatalog, inspectArtifactDirectory } from './artifact-lifecycle.js';
6
6
  import { parseArtifactName } from './artifact-name.js';
7
- import { parseTypedTaskFrontmatter } from './frontmatter.js';
7
+ import { parseTypedTaskFrontmatter, updateTaskFrontmatter } from './frontmatter.js';
8
+ import { upsertSection } from './sections.js';
8
9
  import { locateHotTaskDirs, resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
9
10
  import { configuredShortIdLength, executeShortIdCommand, loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
10
11
  import { captureTaskWriteMetadata, writeTask } from './write.js';
@@ -261,6 +262,31 @@ function mutationsFor(request, content, metadata, restoredFiles, allowMissingAct
261
262
  }
262
263
  return mutations;
263
264
  }
265
+ function previewTaskCompletion(taskRef, agent, options = {}) {
266
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
267
+ if (!resolved.ok)
268
+ throw Object.assign(new Error(resolved.message), { code: resolved.code });
269
+ if (resolved.state !== 'active')
270
+ throw Object.assign(new Error(`task ${resolved.taskId} is ${resolved.state}, expected active`), { code: 'TASK_STATE_MISMATCH' });
271
+ const original = fs.readFileSync(resolved.taskMdPath, 'utf8');
272
+ const metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
273
+ if (!isValidAgentInfraVersion(metadata.agentInfraVersion))
274
+ throw Object.assign(new Error('metadata agentInfraVersion is invalid'), { code: 'LIFECYCLE_METADATA_FAILED' });
275
+ const mutations = mutationsFor({ taskRef: resolved.taskId, intent: 'complete', agent }, original, metadata, 0);
276
+ if (!Array.isArray(mutations))
277
+ throw Object.assign(new Error(mutations.message), { code: mutations.code });
278
+ let content = original;
279
+ for (const mutation of mutations) {
280
+ if (mutation.kind === 'frontmatter')
281
+ content = updateTaskFrontmatter(content, mutation.set, mutation.remove);
282
+ else if (mutation.kind === 'section')
283
+ content = upsertSection(content, mutation).content;
284
+ else
285
+ throw Object.assign(new Error('completion projection contains an unsupported mutation'), { code: 'LIFECYCLE_PROJECTION_INVALID' });
286
+ }
287
+ content = updateTaskFrontmatter(content, { updated_at: metadata.timestamp, agent_infra_version: metadata.agentInfraVersion });
288
+ return { taskId: resolved.taskId, content, timestamp: metadata.timestamp, agentInfraVersion: metadata.agentInfraVersion };
289
+ }
264
290
  function writeJournal(file, journal, io, create = false) {
265
291
  const temporary = `${file}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
266
292
  io.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, 'wx');
@@ -593,6 +619,16 @@ function applyTaskLifecycle(requestInput, options = {}) {
593
619
  completed.add('registry-committed');
594
620
  }
595
621
  let taskOperations = [];
622
+ if (options.prepareOnly && !matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) {
623
+ return {
624
+ ...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
625
+ taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
626
+ task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
627
+ shortId: { effect: 'unchanged', shortId: null, changed: false },
628
+ timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
629
+ journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
630
+ };
631
+ }
596
632
  if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles))
597
633
  completed.add('task-written');
598
634
  if (!completed.has('task-written')) {
@@ -724,5 +760,5 @@ function applyTaskLifecycle(requestInput, options = {}) {
724
760
  journalPath, completedSteps: [...completed], pendingSteps: []
725
761
  };
726
762
  }
727
- export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle };
763
+ export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle, previewTaskCompletion };
728
764
  //# sourceMappingURL=lifecycle.js.map