@fitlab-ai/agent-infra 0.11.6 → 0.11.8

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 (192) hide show
  1. package/dist/bin/internal-cli.js +8 -2
  2. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  3. package/dist/lib/agent-clients/config.js +11 -3
  4. package/dist/lib/defaults.json +6 -3
  5. package/dist/lib/git/primary-worktree.js +58 -0
  6. package/dist/lib/internal/cli-route-inventory.js +3 -0
  7. package/dist/lib/internal/platform-pr-review.js +20 -3
  8. package/dist/lib/internal/task-operation-registry.js +3 -1
  9. package/dist/lib/internal/task-platform-recovery.js +56 -0
  10. package/dist/lib/merge.js +25 -5
  11. package/dist/lib/platform/completion-backfill.js +2 -1
  12. package/dist/lib/platform/in-label-sync.js +12 -1
  13. package/dist/lib/platform/issue-comments.js +58 -4
  14. package/dist/lib/platform/issues.js +105 -5
  15. package/dist/lib/platform/operation-coordinator.js +74 -0
  16. package/dist/lib/platform/pr-review.js +107 -1
  17. package/dist/lib/platform/pr-summary.js +79 -57
  18. package/dist/lib/platform/pull-requests.js +125 -7
  19. package/dist/lib/sandbox/commands/create.js +6 -3
  20. package/dist/lib/sandbox/commands/ls.js +4 -3
  21. package/dist/lib/sandbox/commands/rm.js +1 -1
  22. package/dist/lib/sandbox/config.js +22 -4
  23. package/dist/lib/sandbox/control/server.js +6 -2
  24. package/dist/lib/sandbox/git-metadata.js +50 -0
  25. package/dist/lib/sandbox/index.js +1 -1
  26. package/dist/lib/sandbox/mounts.js +12 -0
  27. package/dist/lib/sandbox/recovery.js +11 -0
  28. package/dist/lib/sandbox/removal.js +58 -20
  29. package/dist/lib/sandbox/tools.js +29 -7
  30. package/dist/lib/server/config.js +7 -27
  31. package/dist/lib/server/daemon.js +4 -3
  32. package/dist/lib/server/process-control.js +4 -2
  33. package/dist/lib/server/runner.js +4 -3
  34. package/dist/lib/task/activity-intent.js +4 -3
  35. package/dist/lib/task/artifact-lifecycle.js +42 -10
  36. package/dist/lib/task/artifact-receipts.js +62 -22
  37. package/dist/lib/task/capabilities.js +22 -4
  38. package/dist/lib/task/completion-facts.js +56 -0
  39. package/dist/lib/task/create-service.js +11 -5
  40. package/dist/lib/task/create.js +60 -79
  41. package/dist/lib/task/events.js +268 -195
  42. package/dist/lib/task/finalization-artifacts.js +4 -1
  43. package/dist/lib/task/finalization-handoff.js +2 -1
  44. package/dist/lib/task/finalization.js +157 -24
  45. package/dist/lib/task/invalidation-command.js +4 -2
  46. package/dist/lib/task/invalidation.js +38 -9
  47. package/dist/lib/task/lifecycle.js +38 -2
  48. package/dist/lib/task/local-artifact-finalization.js +17 -3
  49. package/dist/lib/task/platform-operation-journal.js +152 -0
  50. package/dist/lib/task/platform-operation-recovery.js +459 -0
  51. package/dist/lib/task/qualification-audit.js +23 -105
  52. package/dist/lib/task/review-finalization.js +7 -0
  53. package/dist/lib/task/review-identity.js +69 -0
  54. package/dist/lib/task/sections.js +6 -5
  55. package/dist/lib/task/task-execution-lock.js +6 -3
  56. package/dist/lib/task/verification-engine.js +14 -63
  57. package/dist/lib/task/verification.js +35 -38
  58. package/dist/lib/task/workflow-warning-intents.js +7 -1
  59. package/dist/lifecycle-build-manifest.json +3 -3
  60. package/lib/agent-clients/adapters/codex.ts +1 -1
  61. package/lib/agent-clients/config.ts +9 -3
  62. package/lib/defaults.json +6 -3
  63. package/lib/git/primary-worktree.ts +62 -0
  64. package/lib/internal/cli-route-inventory.ts +2 -0
  65. package/lib/internal/platform-pr-review.ts +14 -3
  66. package/lib/internal/task-operation-registry.ts +3 -1
  67. package/lib/internal/task-platform-recovery.ts +53 -0
  68. package/lib/merge.ts +26 -6
  69. package/lib/platform/completion-backfill.ts +2 -1
  70. package/lib/platform/in-label-sync.ts +12 -0
  71. package/lib/platform/issue-comments.ts +59 -3
  72. package/lib/platform/issues.ts +103 -5
  73. package/lib/platform/operation-coordinator.ts +96 -0
  74. package/lib/platform/pr-review.ts +113 -1
  75. package/lib/platform/pr-summary.ts +22 -1
  76. package/lib/platform/pull-requests.ts +132 -8
  77. package/lib/sandbox/commands/create.ts +7 -8
  78. package/lib/sandbox/commands/ls.ts +4 -3
  79. package/lib/sandbox/commands/rm.ts +1 -1
  80. package/lib/sandbox/config.ts +23 -5
  81. package/lib/sandbox/control/server.ts +5 -2
  82. package/lib/sandbox/git-metadata.ts +66 -0
  83. package/lib/sandbox/index.ts +1 -1
  84. package/lib/sandbox/mounts.ts +14 -0
  85. package/lib/sandbox/recovery.ts +13 -0
  86. package/lib/sandbox/removal.ts +57 -19
  87. package/lib/sandbox/tool-types.ts +1 -1
  88. package/lib/sandbox/tools.ts +36 -7
  89. package/lib/server/config.ts +7 -29
  90. package/lib/server/daemon.ts +5 -3
  91. package/lib/server/process-control.ts +4 -2
  92. package/lib/server/runner.ts +7 -4
  93. package/lib/server/server.schema.json +0 -4
  94. package/lib/task/activity-intent.ts +4 -3
  95. package/lib/task/artifact-lifecycle.ts +35 -11
  96. package/lib/task/artifact-receipts.ts +55 -15
  97. package/lib/task/capabilities.ts +17 -3
  98. package/lib/task/completion-facts.ts +66 -0
  99. package/lib/task/create-service.ts +12 -6
  100. package/lib/task/create.ts +54 -69
  101. package/lib/task/events.ts +248 -182
  102. package/lib/task/finalization-artifacts.ts +4 -1
  103. package/lib/task/finalization-handoff.ts +2 -1
  104. package/lib/task/finalization.ts +154 -21
  105. package/lib/task/invalidation-command.ts +4 -2
  106. package/lib/task/invalidation.ts +35 -10
  107. package/lib/task/lifecycle.ts +35 -3
  108. package/lib/task/local-artifact-finalization.ts +13 -3
  109. package/lib/task/platform-operation-journal.ts +230 -0
  110. package/lib/task/platform-operation-recovery.ts +435 -0
  111. package/lib/task/qualification-audit.ts +21 -114
  112. package/lib/task/review-finalization.ts +7 -0
  113. package/lib/task/review-identity.ts +85 -0
  114. package/lib/task/sections.ts +6 -6
  115. package/lib/task/task-execution-lock.ts +7 -3
  116. package/lib/task/verification-engine.ts +13 -86
  117. package/lib/task/verification.ts +34 -38
  118. package/lib/task/workflow-warning-intents.ts +9 -1
  119. package/package.json +1 -1
  120. package/templates/.agents/README.en.md +19 -17
  121. package/templates/.agents/README.zh-CN.md +18 -16
  122. package/templates/.agents/rules/README.en.md +2 -1
  123. package/templates/.agents/rules/README.zh-CN.md +2 -1
  124. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  125. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  126. package/templates/.agents/rules/task-management.en.md +12 -0
  127. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  128. package/templates/.agents/rules/validation-output.en.md +19 -0
  129. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  130. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  131. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  132. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  133. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  134. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
  135. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
  136. package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
  137. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
  138. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  139. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  140. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  142. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  143. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  144. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  145. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  146. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  147. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  148. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  149. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  150. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  151. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  152. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  153. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  155. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  157. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  158. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  159. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  160. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  161. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  162. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  163. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  164. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  165. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  166. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  167. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  168. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  169. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  170. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  171. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  172. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  173. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  174. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  175. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  176. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  177. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  178. package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
  179. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
  180. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  181. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  182. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  183. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  184. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  191. package/templates/.agents/templates/task.en.md +2 -2
  192. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -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
  });
@@ -637,6 +661,8 @@ async function prepareUnderLock(request, taskId, options) {
637
661
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
638
662
  const backfill = options.backfill ?? backfillCompletionComments;
639
663
  const commentSync = options.commentSync ?? syncPlatformComment;
664
+ const issueSync = options.issueSync ?? syncPlatformIssue;
665
+ const issueInspect = options.issueInspect ?? inspectPlatformIssue;
640
666
  const verify = options.verify ?? verifyTaskEvent;
641
667
  const consumedCapabilities = new Set();
642
668
  let receipt;
@@ -679,6 +705,100 @@ async function prepareUnderLock(request, taskId, options) {
679
705
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_PREFLIGHT_FAILED', true));
680
706
  }
681
707
  }
708
+ if (preflightState.ok && preflightState.state === 'active') {
709
+ const recovery = await recoverPlatformOperations(taskId, 'all', { agent: request.agent, cwd: repoRoot });
710
+ if (recovery.status !== 'applied' && recovery.status !== 'no-op') {
711
+ return failed(taskId, recovery.error ?? {
712
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
713
+ message: 'Pending platform operations remain unresolved',
714
+ retryable: true
715
+ });
716
+ }
717
+ const taskContent = fs.readFileSync(preflightState.taskMdPath, 'utf8');
718
+ const taskFrontmatter = parseTaskFrontmatter(taskContent);
719
+ const issueIdentity = taskIssueIdentity(taskFrontmatter);
720
+ if (issueIdentity) {
721
+ const base = typeof taskFrontmatter.delivery_base_ref === 'string' ? taskFrontmatter.delivery_base_ref.trim() : '';
722
+ let synced;
723
+ try {
724
+ synced = await issueSync(taskId, {
725
+ agent: request.agent, cwd: repoRoot, requirements: true, issueType: true, fields: true,
726
+ ...(base ? { inLabels: 'from-diff', base } : {}), dependency: 'required'
727
+ });
728
+ }
729
+ catch (error) {
730
+ const detail = errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true);
731
+ return failed(taskId, detail);
732
+ }
733
+ const issueOperations = synced.operations;
734
+ const badOperation = issueOperations?.find((operation) => ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])].includes(operation.name)
735
+ && operation.status !== 'applied' && operation.status !== 'no-op');
736
+ if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
737
+ const error = synced.error ?? {
738
+ code: badOperation?.reasonCode || 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
739
+ message: `Required Issue metadata operation '${badOperation?.name ?? 'sync'}' was not confirmed`,
740
+ retryable: true
741
+ };
742
+ return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true));
743
+ }
744
+ let inspected;
745
+ try {
746
+ inspected = await issueInspect(taskId, { cwd: repoRoot });
747
+ }
748
+ catch (error) {
749
+ return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_INSPECT_FAILED', true));
750
+ }
751
+ if (inspected.status !== 'no-op' || inspected.error || !inspected.issue
752
+ || !resourceIdentityEquals(inspected.issue.identity, issueIdentity)) {
753
+ return failed(taskId, inspected.error ?? {
754
+ code: 'FINALIZATION_ISSUE_IDENTITY_UNCONFIRMED',
755
+ message: 'Required Issue metadata could not be reread under the bound Issue identity',
756
+ retryable: true
757
+ });
758
+ }
759
+ for (const name of ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])]) {
760
+ const operation = issueOperations?.find((candidate) => candidate.name === name);
761
+ if (!operation)
762
+ return failed(taskId, {
763
+ code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
764
+ message: `Required Issue metadata operation '${name}' was not reported`, retryable: true
765
+ });
766
+ const before = synced.issue;
767
+ const remote = inspected.issue;
768
+ if (name === 'requirements') {
769
+ const expected = typeof operation.value === 'string' ? operation.value : before?.body;
770
+ if (expected === undefined || remote.body !== expected)
771
+ return failed(taskId, {
772
+ code: 'FINALIZATION_ISSUE_REQUIREMENTS_UNCONFIRMED', message: 'Issue requirements did not match the verified target', retryable: true
773
+ });
774
+ }
775
+ else if (name === 'issue-type') {
776
+ const expected = typeof operation.value === 'string' ? operation.value : before?.issueType;
777
+ if (!expected || remote.issueType !== expected)
778
+ return failed(taskId, {
779
+ code: 'FINALIZATION_ISSUE_TYPE_UNCONFIRMED', message: 'Issue Type did not match the verified target', retryable: true
780
+ });
781
+ }
782
+ else if (name === 'labels:in') {
783
+ const expected = Array.isArray(operation.value) ? operation.value : before?.labels;
784
+ if (!expected || expected.filter((label) => label.startsWith('in:')).sort().join('\0')
785
+ !== remote.labels.filter((label) => label.startsWith('in:')).sort().join('\0'))
786
+ return failed(taskId, {
787
+ code: 'FINALIZATION_IN_LABELS_UNCONFIRMED', message: 'Issue in-labels did not match the verified target', retryable: true
788
+ });
789
+ }
790
+ else {
791
+ const expected = operation.value && typeof operation.value === 'object'
792
+ ? operation.value
793
+ : before?.fields;
794
+ if (!expected || Object.entries(expected).some(([field, value]) => remote.fields[field] !== value))
795
+ return failed(taskId, {
796
+ code: 'FINALIZATION_ISSUE_FIELDS_UNCONFIRMED', message: 'Pinned Issue fields did not match the verified target', retryable: true
797
+ });
798
+ }
799
+ }
800
+ }
801
+ }
682
802
  let changed = false;
683
803
  let backfillResult = null;
684
804
  let runBackfill = shouldRunBackfill(receipt);
@@ -748,9 +868,10 @@ async function prepareUnderLock(request, taskId, options) {
748
868
  else
749
869
  backfillResult = { status: 'no-op', changed: false, error: null };
750
870
  let lifecycleResult = null;
871
+ let projection;
751
872
  try {
752
873
  const resolved = resolveTaskRef(taskId, { repoRoot });
753
- if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
874
+ if (resolved.ok && resolved.state === 'completed') {
754
875
  lifecycleResult = { status: 'no-op', changed: false, error: null };
755
876
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
756
877
  }
@@ -762,6 +883,12 @@ async function prepareUnderLock(request, taskId, options) {
762
883
  return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
763
884
  }
764
885
  changed = changed || result.changed;
886
+ if (result.timestamp && result.agentInfraVersion) {
887
+ projection = previewTaskCompletion(taskId, request.agent, {
888
+ repoRoot,
889
+ metadataProvider: () => ({ timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion })
890
+ });
891
+ }
765
892
  }
766
893
  }
767
894
  catch (error) {
@@ -774,6 +901,12 @@ async function prepareUnderLock(request, taskId, options) {
774
901
  }
775
902
  let taskComment = null;
776
903
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
904
+ const currentProjection = () => projection
905
+ ? previewTaskCompletion(taskId, request.agent, {
906
+ repoRoot,
907
+ metadataProvider: () => ({ timestamp: projection.timestamp, agentInfraVersion: projection.agentInfraVersion })
908
+ })
909
+ : undefined;
777
910
  let verification = null;
778
911
  if (receipt.verification !== 'pending') {
779
912
  verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
@@ -796,7 +929,7 @@ async function prepareUnderLock(request, taskId, options) {
796
929
  });
797
930
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
798
931
  const finalComment = await syncPendingTaskComment({
799
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
932
+ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
800
933
  });
801
934
  receipt = finalComment.receipt;
802
935
  if (finalComment.step.status !== 'no-op')
@@ -814,7 +947,7 @@ async function prepareUnderLock(request, taskId, options) {
814
947
  });
815
948
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
816
949
  const warningComment = await syncPendingTaskComment({
817
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
950
+ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
818
951
  });
819
952
  receipt = warningComment.receipt;
820
953
  taskComment = warningComment.step;
@@ -846,7 +979,7 @@ async function prepareUnderLock(request, taskId, options) {
846
979
  }, changed, hardError);
847
980
  }
848
981
  try {
849
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
982
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities });
850
983
  receipt = warningComment.receipt;
851
984
  if (warningComment.step.status !== 'no-op')
852
985
  taskComment = warningComment.step;
@@ -868,7 +1001,7 @@ async function prepareUnderLock(request, taskId, options) {
868
1001
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
869
1002
  if (receipt.taskComment === 'pending') {
870
1003
  const finalComment = await syncPendingTaskComment({
871
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
1004
+ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
872
1005
  });
873
1006
  receipt = finalComment.receipt;
874
1007
  taskComment = finalComment.step;
@@ -38,8 +38,10 @@ function reconcileTaskInvalidation(taskRef, options = {}) {
38
38
  if (target.status === 'completed')
39
39
  continue;
40
40
  if (target.targetKind === 'receipt') {
41
- const receipt = receipts.find((candidate) => candidate.output === target.targetArtifact);
42
- if (!receipt || receipt.inputSha256 !== target.targetSha256) {
41
+ const receiptExists = Boolean(target.targetInput) && receipts.some((candidate) => candidate.output === target.targetArtifact
42
+ && candidate.input === target.targetInput
43
+ && candidate.inputSha256 === target.targetSha256);
44
+ if (!receiptExists) {
43
45
  return {
44
46
  status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending,
45
47
  error: { code: 'INVALIDATION_TARGET_HASH_CONFLICT', message: `invalidation receipt target '${target.targetArtifact}' changed before reconcile` }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { parseArtifactName } from './artifact-name.js';
2
3
  import { findSectionRange, parseTable } from './sections.js';
3
4
  import { scanVisibleMarkdown } from './markdown.js';
4
5
  const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'];
@@ -7,6 +8,10 @@ const OPERATION_COLUMNS = [
7
8
  'status', 'processed', 'total', 'created_at', 'updated_at', 'completed_at', 'error'
8
9
  ];
9
10
  const TARGET_COLUMNS = [
11
+ 'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact', 'target_input',
12
+ 'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
13
+ ];
14
+ const LEGACY_TARGET_COLUMNS = [
10
15
  'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact',
11
16
  'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
12
17
  ];
@@ -20,7 +25,7 @@ function operationIdFor(source) {
20
25
  return `INV-${hashIdentity([source.sourceFamily, source.sourceArtifact, source.sourceRound, source.sourceSha256])}`;
21
26
  }
22
27
  function targetIdFor(operationId, target) {
23
- return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, target.targetRound, target.targetSha256])}`;
28
+ return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, ...(target.targetKind === 'receipt' && target.targetInput ? [target.targetInput] : []), target.targetRound, target.targetSha256])}`;
24
29
  }
25
30
  function createInvalidationOperation(source, targets = []) {
26
31
  const operationId = source.operationId ?? operationIdFor(source);
@@ -31,14 +36,28 @@ function createInvalidationOperation(source, targets = []) {
31
36
  createdAt: source.createdAt, updatedAt: source.updatedAt, completedAt: '', error: ''
32
37
  };
33
38
  }
34
- function parseInvalidationTable(body, heading, columns) {
39
+ function parseInvalidationTable(body, heading, columnChoices) {
35
40
  const section = findSectionRange(body, [heading], 3);
36
41
  const lines = scanVisibleMarkdown(body).lines.filter((line) => line.text.trim());
37
- const table = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
42
+ let table = null;
43
+ let legacy = false;
44
+ for (const columns of columnChoices) {
45
+ try {
46
+ const candidate = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
47
+ if (candidate) {
48
+ table = candidate;
49
+ legacy = columns === LEGACY_TARGET_COLUMNS;
50
+ break;
51
+ }
52
+ }
53
+ catch {
54
+ // Try the next explicitly supported schema.
55
+ }
56
+ }
38
57
  if (!section || !table || lines.filter((line) => line.start >= section.bodyStart && line.start < section.end).length !== table.rows.length + 2) {
39
58
  throw new Error(`invalidation ${heading} section must contain only its canonical table`);
40
59
  }
41
- return table.rows;
60
+ return { rows: table.rows, legacy };
42
61
  }
43
62
  function required(value, field) {
44
63
  if (!value)
@@ -62,9 +81,10 @@ function parseInvalidationDocument(content) {
62
81
  return { ok: true, present: false, document: { operations: [], targets: [] } };
63
82
  try {
64
83
  const body = content.slice(section.bodyStart, section.end);
65
- const operationRows = parseInvalidationTable(body, 'Operations', OPERATION_COLUMNS);
66
- const targetRows = parseInvalidationTable(body, 'Targets', TARGET_COLUMNS);
67
- const operations = operationRows.map(({ values: row }) => ({
84
+ const operationRows = parseInvalidationTable(body, 'Operations', [OPERATION_COLUMNS]);
85
+ // TODO(compat): Remove legacy target table parsing and completed receipt targets without target_input once a workspace inventory confirms all persisted targets have input identities.
86
+ const targetRows = parseInvalidationTable(body, 'Targets', [TARGET_COLUMNS, LEGACY_TARGET_COLUMNS]);
87
+ const operations = operationRows.rows.map(({ values: row }) => ({
68
88
  operationId: required(row.operation_id, 'operation_id'), sourceFamily: required(row.source_family, 'source_family'),
69
89
  sourceArtifact: required(row.source_artifact, 'source_artifact'), sourceRound: numberValue(row.source_round, 'source_round', false),
70
90
  sourceSha256: required(row.source_sha256, 'source_sha256'), status: status(row.status, 'operation status'),
@@ -72,10 +92,11 @@ function parseInvalidationDocument(content) {
72
92
  createdAt: required(row.created_at, 'created_at'), updatedAt: required(row.updated_at, 'updated_at'),
73
93
  completedAt: row.completed_at ?? '', error: row.error ?? ''
74
94
  }));
75
- const targets = targetRows.map(({ values: row }) => ({
95
+ const targets = targetRows.rows.map(({ values: row }) => ({
76
96
  targetId: required(row.target_id, 'target_id'), operationId: required(row.operation_id, 'operation_id'),
77
97
  targetKind: row.target_kind, targetFamily: required(row.target_family, 'target_family'),
78
98
  targetArtifact: required(row.target_artifact, 'target_artifact'), targetRound: numberValue(row.target_round, 'target_round', false),
99
+ ...(row.target_input ? { targetInput: row.target_input } : {}),
79
100
  targetSha256: required(row.target_sha256, 'target_sha256'), status: status(row.status, 'target status'),
80
101
  reasonCode: required(row.reason_code, 'reason_code'), updatedAt: required(row.updated_at, 'updated_at')
81
102
  }));
@@ -100,6 +121,14 @@ function parseInvalidationDocument(content) {
100
121
  targetIds.add(target.targetId);
101
122
  if (!operationIds.has(target.operationId))
102
123
  throw new Error(`target '${target.targetId}' references an unknown operation`);
124
+ if (target.targetKind === 'receipt') {
125
+ if (!target.targetInput && target.status !== 'completed')
126
+ throw new Error(`receipt target '${target.targetId}' input identity is required`);
127
+ if (target.targetInput && !parseArtifactName(target.targetInput))
128
+ throw new Error(`receipt target '${target.targetId}' input identity is invalid`);
129
+ }
130
+ else if (target.targetInput)
131
+ throw new Error(`non-receipt target '${target.targetId}' cannot have an input identity`);
103
132
  if (target.targetId !== targetIdFor(target.operationId, target))
104
133
  throw new Error(`target '${target.targetId}' has a non-canonical identity`);
105
134
  if (!/^[a-f0-9]{64}$/.test(target.targetSha256))
@@ -143,7 +172,7 @@ function renderInvalidation(document) {
143
172
  '### Targets', '',
144
173
  table(TARGET_COLUMNS, targets.map((row) => [
145
174
  row.targetId, row.operationId, row.targetKind, row.targetFamily, row.targetArtifact,
146
- row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
175
+ row.targetInput ?? '', row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
147
176
  ]))
148
177
  ].join('\n');
149
178
  }
@@ -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
@@ -6,6 +6,7 @@ import { parseLifecyclePathDecision } from './lifecycle-path.js';
6
6
  import { resolveTaskRef } from './resolve-ref.js';
7
7
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
8
8
  import { getArtifactSchema } from './artifact-schema.js';
9
+ import { validateQualificationAudit } from './qualification-audit.js';
9
10
  const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
10
11
  analysis: getArtifactSchema('analysis').sections.map((section) => section.headings.zh),
11
12
  plan: getArtifactSchema('plan').sections.map((section) => section.headings.zh),
@@ -82,19 +83,32 @@ function prepareLocalArtifact(request, candidate) {
82
83
  const validation = validateLocalArtifact(content, {
83
84
  family: request.family, requiredSections: request.requiredSections
84
85
  });
86
+ let qualificationError = null;
87
+ try {
88
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
89
+ const qualification = validateQualificationAudit(taskContent, content, { family: request.family, artifact: request.artifact });
90
+ if (!qualification.ok)
91
+ qualificationError = `${qualification.code}: ${qualification.message}`;
92
+ }
93
+ catch (error) {
94
+ qualificationError = `QUALIFICATION_AUDIT_INVALID: ${String(error)}`;
95
+ }
85
96
  const artifactSha256 = sha256Content(content);
97
+ const diagnostics = qualificationError
98
+ ? [...validation.diagnostics, { code: 'LOCAL_STRUCTURAL_INVALID', message: qualificationError, line: null }]
99
+ : validation.diagnostics;
86
100
  const result = failedFinalization(request, {
87
101
  code: 'LOCAL_ARTIFACT_INVALID',
88
- message: validation.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
102
+ message: diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
89
103
  }, {
90
104
  taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256,
91
- semanticDigest: validation.semanticDigest, diagnostics: validation.diagnostics
105
+ semanticDigest: validation.semanticDigest, diagnostics
92
106
  });
93
107
  // Trusted local operators edit the canonical artifact directly. A
94
108
  // finalization result is a fresh observation of those bytes, never a
95
109
  // capability minted by an earlier recovery attempt or generation.
96
110
  return {
97
- result: validation.ok ? { ...result, status: 'passed', error: null } : result,
111
+ result: validation.ok && !qualificationError ? { ...result, status: 'passed', error: null } : result,
98
112
  content,
99
113
  repoRoot: resolved.repoRoot,
100
114
  lockAlreadyHeld: request.lockAlreadyHeld