@fitlab-ai/agent-infra 0.11.0 → 0.11.2

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 (146) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/manual-validation.js +6 -2
  3. package/dist/lib/internal/platform-comment.js +59 -0
  4. package/dist/lib/internal/sandbox-control.js +4 -131
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-operation-registry.js +1 -0
  7. package/dist/lib/internal/task-review.js +5 -5
  8. package/dist/lib/internal/task-validate.js +0 -6
  9. package/dist/lib/platform/comment-safety.js +7 -1
  10. package/dist/lib/platform/github-provider.js +13 -3
  11. package/dist/lib/platform/issue-comments.js +120 -13
  12. package/dist/lib/platform/issues.js +3 -4
  13. package/dist/lib/platform/provider-contract.d.ts +2 -0
  14. package/dist/lib/platform/provider-validation.js +7 -2
  15. package/dist/lib/platform/verification-sync.js +161 -156
  16. package/dist/lib/sandbox/control/executor.js +0 -38
  17. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  18. package/dist/lib/sandbox/control/task-view.js +3 -1
  19. package/dist/lib/sandbox/control/workflow-executor.js +32 -41
  20. package/dist/lib/task/artifact-command.js +6 -6
  21. package/dist/lib/task/artifact-lifecycle.js +15 -52
  22. package/dist/lib/task/artifact-operations.js +27 -1
  23. package/dist/lib/task/capabilities.js +0 -2
  24. package/dist/lib/task/codex-orchestration.js +10 -208
  25. package/dist/lib/task/commit-operation.js +3 -18
  26. package/dist/lib/task/control-authority.js +1 -8
  27. package/dist/lib/task/delegation-receipts.js +14 -76
  28. package/dist/lib/task/events.js +147 -278
  29. package/dist/lib/task/finalization-state.js +2 -1
  30. package/dist/lib/task/finalization.js +273 -49
  31. package/dist/lib/task/gate-policy.js +94 -0
  32. package/dist/lib/task/lifecycle-execution.js +2 -11
  33. package/dist/lib/task/local-artifact-finalization.js +31 -245
  34. package/dist/lib/task/orchestration.js +8 -24
  35. package/dist/lib/task/review-artifacts.js +12 -6
  36. package/dist/lib/task/review-command.js +0 -1
  37. package/dist/lib/task/review-finalization.js +47 -218
  38. package/dist/lib/task/verification-engine.js +28 -17
  39. package/dist/lib/task/verification.js +25 -9
  40. package/dist/lib/task/workflow-command.js +4 -1
  41. package/dist/lib/task/workflow-dispatch.js +55 -3
  42. package/dist/lifecycle-build-manifest.json +3 -3
  43. package/lib/internal/cli-route-inventory.ts +1 -1
  44. package/lib/internal/manual-validation.ts +6 -2
  45. package/lib/internal/platform-comment.ts +38 -0
  46. package/lib/internal/sandbox-control.ts +3 -145
  47. package/lib/internal/task-artifact.ts +1 -1
  48. package/lib/internal/task-operation-registry.ts +1 -0
  49. package/lib/internal/task-review.ts +5 -5
  50. package/lib/internal/task-validate.ts +0 -5
  51. package/lib/platform/comment-safety.ts +8 -0
  52. package/lib/platform/github-provider.ts +11 -2
  53. package/lib/platform/issue-comments.ts +132 -13
  54. package/lib/platform/issues.ts +3 -4
  55. package/lib/platform/provider-contract.ts +2 -0
  56. package/lib/platform/provider-validation.ts +6 -2
  57. package/lib/platform/verification-sync.ts +178 -179
  58. package/lib/sandbox/control/executor.ts +0 -44
  59. package/lib/sandbox/control/finalization-response.ts +10 -2
  60. package/lib/sandbox/control/task-view.ts +3 -1
  61. package/lib/sandbox/control/workflow-executor.ts +31 -48
  62. package/lib/task/artifact-command.ts +6 -7
  63. package/lib/task/artifact-lifecycle.ts +12 -49
  64. package/lib/task/artifact-operations.ts +27 -1
  65. package/lib/task/capabilities.ts +0 -1
  66. package/lib/task/codex-orchestration.ts +8 -241
  67. package/lib/task/commit-operation.ts +3 -23
  68. package/lib/task/control-authority.ts +1 -8
  69. package/lib/task/delegation-receipts.ts +12 -79
  70. package/lib/task/events.ts +155 -287
  71. package/lib/task/finalization-state.ts +2 -1
  72. package/lib/task/finalization.ts +277 -57
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/lifecycle-execution.ts +10 -21
  75. package/lib/task/local-artifact-finalization.ts +29 -285
  76. package/lib/task/orchestration.ts +12 -41
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +0 -1
  79. package/lib/task/review-finalization.ts +46 -200
  80. package/lib/task/verification-engine.ts +24 -14
  81. package/lib/task/verification-types.ts +7 -0
  82. package/lib/task/verification.ts +25 -9
  83. package/lib/task/workflow-command.ts +4 -1
  84. package/lib/task/workflow-dispatch.ts +46 -3
  85. package/package.json +2 -2
  86. package/templates/.agents/rules/issue-sync.en.md +2 -0
  87. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
  88. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  89. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  90. package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
  91. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
  92. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  93. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  94. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  95. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  96. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  97. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  98. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  101. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  102. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  104. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  105. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  106. package/templates/.agents/skills/commit/config/verify.json +1 -5
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  108. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  109. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  110. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  111. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  112. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  113. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  114. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  115. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  116. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  117. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  118. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  119. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  120. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
  121. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  122. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  123. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  124. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  125. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  126. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  127. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  128. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  129. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  130. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  131. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  132. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  133. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  134. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  135. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  136. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  137. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  138. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  139. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  140. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  141. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  142. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  143. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  144. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  145. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  146. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -1,9 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
4
 
5
5
  import { syncPlatformComment } from '../platform/issue-comments.ts';
6
+ import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
6
7
  import type { PlatformResult } from '../platform/types.ts';
8
+ import { taskIssueIdentity } from '../platform/task-identities.ts';
9
+ import { parseTaskFrontmatter } from './frontmatter.ts';
7
10
  import {
8
11
  applyTaskLifecycle,
9
12
  type TaskLifecycleOptions,
@@ -22,8 +25,8 @@ import {
22
25
  type OperationWarningSeverity
23
26
  } from './operation-outcome.ts';
24
27
 
25
- const RECEIPT_VERSION = 2 as const;
26
- const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification'] as const;
28
+ const RECEIPT_VERSION = 3 as const;
29
+ const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary', 'post-summary-verification'] as const;
27
30
  type FinalizationStep = typeof FINALIZATION_STEPS[number];
28
31
  type FinalizationStepState = 'pending' | 'done' | 'skipped';
29
32
  type FinalizationError = { code: string; message: string; retryable: boolean };
@@ -72,8 +75,11 @@ type TaskFinalizationReceipt = Readonly<{
72
75
  lifecycle: FinalizationStepState;
73
76
  taskComment: FinalizationStepState;
74
77
  verification: FinalizationStepState;
78
+ summary: FinalizationStepState;
79
+ postSummaryVerification: FinalizationStepState;
75
80
  warningProjection: WarningProjectionState;
76
81
  warnings: readonly FinalizationWarning[];
82
+ summarySha256: string | null;
77
83
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
78
84
  updatedAt: string;
79
85
  lastError: FinalizationError | null;
@@ -101,6 +107,8 @@ type TaskFinalizationResult = Readonly<{
101
107
  lifecycle: TaskFinalizationStep | null;
102
108
  taskComment: TaskFinalizationStep | null;
103
109
  verification: TaskFinalizationStep | null;
110
+ summary: TaskFinalizationStep | null;
111
+ postSummaryVerification: TaskFinalizationStep | null;
104
112
  completedSteps: readonly FinalizationStep[];
105
113
  pendingSteps: readonly FinalizationStep[];
106
114
  result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
@@ -139,6 +147,8 @@ function failed(
139
147
  lifecycle: null,
140
148
  taskComment: null,
141
149
  verification: null,
150
+ summary: null,
151
+ postSummaryVerification: null,
142
152
  completedSteps: [],
143
153
  pendingSteps: [...FINALIZATION_STEPS],
144
154
  result: error.retryable ? 'blocked' : 'failed',
@@ -158,8 +168,11 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
158
168
  lifecycle: 'pending',
159
169
  taskComment: 'pending',
160
170
  verification: 'pending',
171
+ summary: 'pending',
172
+ postSummaryVerification: 'pending',
161
173
  warningProjection: 'done',
162
174
  warnings: [],
175
+ summarySha256: null,
163
176
  ...(controlBinding ? { controlBinding } : {}),
164
177
  updatedAt: now(),
165
178
  lastError: null
@@ -194,8 +207,11 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
194
207
  || !states.includes(String(receipt.lifecycle))
195
208
  || !states.includes(String(receipt.taskComment))
196
209
  || !states.includes(String(receipt.verification))
210
+ || !states.includes(String(receipt.summary))
211
+ || !states.includes(String(receipt.postSummaryVerification))
197
212
  || !['pending', 'done'].includes(String(receipt.warningProjection))
198
213
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
214
+ || (receipt.summarySha256 !== null && (typeof receipt.summarySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(receipt.summarySha256)))
199
215
  || typeof receipt.updatedAt !== 'string'
200
216
  || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
201
217
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
@@ -217,14 +233,6 @@ function readTaskFinalizationReceipt(repoRoot: string, taskId: string): TaskFina
217
233
  return readReceipt(path.resolve(repoRoot), taskId);
218
234
  }
219
235
 
220
- function controlBindingMatches(
221
- receipt: TaskFinalizationReceipt,
222
- binding: Readonly<{ generation: string; requestId: string }>
223
- ): boolean {
224
- return receipt.controlBinding?.generation === binding.generation
225
- && receipt.controlBinding.requestId === binding.requestId;
226
- }
227
-
228
236
  function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void {
229
237
  const directory = finalizationRoot(repoRoot);
230
238
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
@@ -242,25 +250,30 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
242
250
  function updateReceipt(
243
251
  repoRoot: string,
244
252
  receipt: TaskFinalizationReceipt,
245
- patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'warningProjection' | 'warnings' | 'lastError'>>
253
+ patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification' | 'warningProjection' | 'warnings' | 'summarySha256' | 'controlBinding' | 'lastError'>>
246
254
  ): TaskFinalizationReceipt {
247
255
  const current = readReceipt(repoRoot, receipt.taskId);
248
- if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
249
- const error = new Error('finalization receipt revision is stale');
250
- Object.assign(error, { code: 'FINALIZATION_CAPABILITY_STALE' });
251
- throw error;
252
- }
256
+ if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
257
+ 'FINALIZATION_SCOPE_INVALID',
258
+ 'finalization receipt does not belong to the current task'
259
+ );
253
260
  const next = { ...current, ...patch, revision: current.revision + 1, updatedAt: now() };
254
261
  writeReceipt(repoRoot, next);
255
262
  return next;
256
263
  }
257
264
 
258
265
  function completedSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
259
- return FINALIZATION_STEPS.filter((step) => receipt[step === 'task-comment' ? 'taskComment' : step] !== 'pending');
266
+ return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) !== 'pending');
260
267
  }
261
268
 
262
269
  function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
263
- return FINALIZATION_STEPS.filter((step) => receipt[step === 'task-comment' ? 'taskComment' : step] === 'pending');
270
+ return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) === 'pending');
271
+ }
272
+
273
+ function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
274
+ if (step === 'task-comment') return receipt.taskComment;
275
+ if (step === 'post-summary-verification') return receipt.postSummaryVerification;
276
+ return receipt[step];
264
277
  }
265
278
 
266
279
  function lifecycleStep(result: TaskLifecycleResult): TaskFinalizationStep {
@@ -376,9 +389,10 @@ function validateCapabilityMutation(
376
389
  mutation: FinalizationMutation,
377
390
  consumed: Set<string>
378
391
  ): void {
379
- if (capability.receiptId !== current.receiptId || capability.baseRevision !== current.revision || consumed.has(capability.nonce)) {
380
- throw capabilityError('FINALIZATION_CAPABILITY_STALE', 'finalization capability is stale or outside its scope');
381
- }
392
+ if (capability.receiptId !== current.receiptId) throw capabilityError(
393
+ 'FINALIZATION_SCOPE_INVALID',
394
+ 'finalization mutation belongs to a different task receipt'
395
+ );
382
396
  if (capability.scope !== mutation.scope) {
383
397
  throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation scope does not match its capability');
384
398
  }
@@ -556,15 +570,98 @@ async function syncPendingTaskComment(input: {
556
570
  }
557
571
  }
558
572
 
573
+ function readDeliverySummary(taskDir: string, taskId: string): { body: string; sha256: string } {
574
+ const file = path.join(taskDir, '.delivery-summary.json');
575
+ let value: Record<string, unknown>;
576
+ try {
577
+ value = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;
578
+ } catch {
579
+ throw Object.assign(new Error('delivery summary staging record is unavailable'), { code: 'SUMMARY_STAGING_INVALID' });
580
+ }
581
+ if (value.taskId !== taskId || typeof value.body !== 'string' || typeof value.sha256 !== 'string') {
582
+ throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
583
+ }
584
+ const canonical = canonicalizeSummaryBody(value.body);
585
+ if (!canonical.ok || canonical.value !== value.body) {
586
+ throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
587
+ }
588
+ const digest = createHash('sha256').update(canonical.value).digest('hex');
589
+ if (digest !== value.sha256) throw Object.assign(new Error('delivery summary staging digest does not match body'), { code: 'SUMMARY_STAGING_INVALID' });
590
+ return { body: canonical.value, sha256: value.sha256 };
591
+ }
592
+
593
+ async function syncPendingSummary(input: {
594
+ repoRoot: string;
595
+ taskId: string;
596
+ agent: string;
597
+ receipt: TaskFinalizationReceipt;
598
+ commentSync: typeof syncPlatformComment;
599
+ }): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
600
+ let { receipt } = input;
601
+ if (receipt.summary !== 'pending') {
602
+ return { receipt, step: { status: receipt.summary === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null }, changed: false, error: null };
603
+ }
604
+ try {
605
+ const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
606
+ if (!resolved.ok || resolved.state !== 'completed') throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
607
+ if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
608
+ receipt = updateReceipt(input.repoRoot, receipt, {
609
+ summary: 'skipped', postSummaryVerification: 'skipped', summarySha256: null, lastError: null
610
+ });
611
+ return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
612
+ }
613
+ const summary = readDeliverySummary(resolved.taskDir, input.taskId);
614
+ const result = await input.commentSync(input.taskId, {
615
+ kind: 'summary', agent: input.agent, body: summary.body, cwd: input.repoRoot,
616
+ summaryAuthorization: { sha256: summary.sha256 }
617
+ });
618
+ const step = commentStep(result);
619
+ if (result.status === 'applied' || result.status === 'no-op') {
620
+ const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
621
+ const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
622
+ const warnings = resolveStepWarnings(receipt, 'summary');
623
+ receipt = updateReceipt(input.repoRoot, receipt, {
624
+ summary: skipped ? 'skipped' : 'done', postSummaryVerification: skipped ? 'skipped' : receipt.postSummaryVerification,
625
+ summarySha256: skipped ? null : summary.sha256,
626
+ taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
627
+ warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
628
+ warnings,
629
+ lastError: null
630
+ });
631
+ return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
632
+ }
633
+ const error = step.error ?? { code: 'SUMMARY_SYNC_FAILED', message: 'delivery summary synchronization failed', retryable: true };
634
+ const warning = warningFromError('summary', error);
635
+ const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
636
+ const warnings = replaceWarning(receipt, warning, 'open');
637
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
638
+ return { receipt, step, changed: result.changed, error };
639
+ } catch (cause) {
640
+ const error = errorOf(cause, 'SUMMARY_SYNC_FAILED', true);
641
+ try {
642
+ const warning = warningFromError('summary', error);
643
+ const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
644
+ const warnings = replaceWarning(receipt, warning, 'open');
645
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
646
+ } catch { /* preserve primary error */ }
647
+ return { receipt, step: { status: 'blocked', changed: false, error }, changed: false, error };
648
+ }
649
+ }
650
+
651
+ function clearDeliverySummary(taskDir: string): void {
652
+ const file = path.join(taskDir, '.delivery-summary.json');
653
+ if (fs.existsSync(file)) fs.unlinkSync(file);
654
+ }
655
+
559
656
  function terminalResult(
560
657
  taskId: string,
561
658
  receipt: TaskFinalizationReceipt,
562
- steps: Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification'>,
659
+ steps: Partial<Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification'>>,
563
660
  changed: boolean,
564
661
  error: FinalizationError | null = null
565
662
  ): TaskFinalizationResult {
566
663
  const pending = pendingSteps(receipt);
567
- const blocked = [steps.lifecycle, steps.taskComment, steps.verification].some((step) => step?.status === 'blocked');
664
+ const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary, steps.postSummaryVerification].some((step) => step?.status === 'blocked');
568
665
  const warnings = openWarnings(receipt);
569
666
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
570
667
  const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
@@ -572,13 +669,17 @@ function terminalResult(
572
669
  status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
573
670
  changed,
574
671
  taskId,
575
- ...steps,
672
+ lifecycle: steps.lifecycle ?? null,
673
+ taskComment: steps.taskComment ?? null,
674
+ verification: steps.verification ?? null,
675
+ summary: steps.summary ?? null,
676
+ postSummaryVerification: steps.postSummaryVerification ?? null,
576
677
  completedSteps: completedSteps(receipt),
577
678
  pendingSteps: pending,
578
- result: postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
579
- ? 'completed_with_warnings'
580
- : hardError
581
- ? (error?.retryable ? 'blocked' : 'failed')
679
+ result: hardError
680
+ ? (error?.retryable ? 'blocked' : 'failed')
681
+ : postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
682
+ ? 'completed_with_warnings'
582
683
  : pending.length === 0 || postLifecyclePending
583
684
  ? 'completed'
584
685
  : blocked
@@ -608,17 +709,10 @@ async function applyUnderLock(
608
709
  } catch (error) {
609
710
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
610
711
  }
611
- if (options.controlBinding && !controlBindingMatches(receipt, options.controlBinding)) {
612
- const error: FinalizationError = {
613
- code: 'TASK_FINALIZATION_CONTROL_BINDING_CONFLICT',
614
- message: 'finalization receipt belongs to a different sandbox request or generation',
615
- retryable: false
616
- };
617
- return failed(taskId, error, {
618
- completedSteps: completedSteps(receipt),
619
- pendingSteps: pendingSteps(receipt)
620
- });
621
- }
712
+ if (options.controlBinding && (
713
+ receipt.controlBinding?.generation !== options.controlBinding.generation
714
+ || receipt.controlBinding.requestId !== options.controlBinding.requestId
715
+ )) receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
622
716
 
623
717
  const preflightState = resolveTaskRef(taskId, { repoRoot });
624
718
  if (options.preflight && preflightState.ok && preflightState.state === 'active') {
@@ -712,15 +806,14 @@ async function applyUnderLock(
712
806
  let verification: TaskFinalizationStep | null = null;
713
807
  try {
714
808
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
715
- if (receipt.verification !== 'pending') {
716
- verification = { status: 'no-op', changed: false, error: null };
717
- } else {
718
- const result = await verify(
719
- { taskRef: taskId, event: 'complete-task.completed' },
720
- { repoRoot }
721
- );
722
- verification = verificationStep(result);
723
- if (result.status === 'pass') {
809
+ const verificationPending = receipt.verification === 'pending';
810
+ const result = await verify(
811
+ { taskRef: taskId, event: 'complete-task.completed' },
812
+ { repoRoot }
813
+ );
814
+ verification = verificationStep(result);
815
+ if (result.status === 'pass') {
816
+ if (verificationPending) {
724
817
  const capability = issueCapability(receipt, 'verification');
725
818
  receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
726
819
  scope: 'verification', operation: 'succeeded'
@@ -736,28 +829,154 @@ async function applyUnderLock(
736
829
  return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
737
830
  }
738
831
  } else {
739
- const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
832
+ receipt = updateReceipt(repoRoot, receipt, { lastError: null });
833
+ }
834
+ } else {
835
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
836
+ if (verificationPending) {
740
837
  const capability = issueCapability(receipt, 'verification');
741
838
  receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
742
839
  scope: 'verification', operation: 'failed', error: detail
743
840
  }, consumedCapabilities);
744
841
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
745
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, verification.error);
842
+ } else {
843
+ const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
844
+ receipt = updateReceipt(repoRoot, receipt, {
845
+ verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
846
+ });
847
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
848
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
849
+ receipt = warningComment.receipt;
850
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
851
+ changed = changed || warningComment.changed;
746
852
  }
853
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
747
854
  }
748
855
  } catch (error) {
749
856
  const detail = errorOf(error, 'VERIFY_FAILED', true);
750
857
  try {
751
- const capability = issueCapability(receipt, 'verification');
752
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
753
- scope: 'verification', operation: 'failed', error: detail
754
- }, consumedCapabilities);
858
+ if (receipt.verification === 'pending') {
859
+ const capability = issueCapability(receipt, 'verification');
860
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
861
+ scope: 'verification', operation: 'failed', error: detail
862
+ }, consumedCapabilities);
863
+ } else {
864
+ const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
865
+ receipt = updateReceipt(repoRoot, receipt, {
866
+ verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
867
+ });
868
+ }
755
869
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
870
+ } catch (writeError) {
871
+ const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
872
+ const hardError: FinalizationError = {
873
+ code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
874
+ message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
875
+ retryable: true
876
+ };
877
+ return terminalResult(taskId, receipt, {
878
+ lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
879
+ }, changed, hardError);
880
+ }
881
+ try {
882
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
883
+ receipt = warningComment.receipt;
884
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
885
+ changed = changed || warningComment.changed;
756
886
  } catch { /* preserve the primary error */ }
757
887
  return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
758
888
  }
889
+
890
+ const summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
891
+ receipt = summary.receipt;
892
+ changed = changed || summary.changed;
893
+ if (summary.error) {
894
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
895
+ const warningComment = await syncPendingTaskComment({
896
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
897
+ });
898
+ receipt = warningComment.receipt;
899
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
900
+ changed = changed || warningComment.changed;
901
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
902
+ }
903
+
904
+ let postSummaryVerification: TaskFinalizationStep | null = null;
905
+ if (receipt.postSummaryVerification === 'skipped') {
906
+ postSummaryVerification = { status: 'skipped', changed: false, error: null };
907
+ } else if (receipt.postSummaryVerification !== 'pending') {
908
+ const replay = await commentSync(taskId, {
909
+ kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
910
+ summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
911
+ });
912
+ postSummaryVerification = commentStep(replay);
913
+ if (replay.status !== 'applied' && replay.status !== 'no-op') {
914
+ const error = postSummaryVerification.error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary replay verification failed', retryable: true };
915
+ const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
916
+ receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
917
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
918
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
919
+ }
920
+ } else {
921
+ try {
922
+ const summaryVerification = await commentSync(taskId, {
923
+ kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
924
+ summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
925
+ });
926
+ if (summaryVerification.status !== 'applied' && summaryVerification.status !== 'no-op') {
927
+ const error = commentStep(summaryVerification).error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary verification failed', retryable: true };
928
+ const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
929
+ receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
930
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
931
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
932
+ receipt = warningComment.receipt;
933
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
934
+ changed = changed || warningComment.changed;
935
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: commentStep(summaryVerification) }, changed, error);
936
+ }
937
+ const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
938
+ postSummaryVerification = verificationStep(result);
939
+ if (result.status === 'pass') {
940
+ const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'post-summary-verification' && warning.status === 'open');
941
+ const warnings = resolveStepWarnings(receipt, 'post-summary-verification');
942
+ receipt = updateReceipt(repoRoot, receipt, {
943
+ postSummaryVerification: 'done', taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
944
+ warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done', warnings, lastError: null
945
+ });
946
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
947
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
948
+ receipt = warningComment.receipt;
949
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
950
+ changed = changed || warningComment.changed;
951
+ } else {
952
+ const error = postSummaryVerification.error ?? { code: 'VERIFY_FAILED', message: 'post-summary verification failed', retryable: true };
953
+ const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
954
+ receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
955
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
956
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
957
+ receipt = warningComment.receipt;
958
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
959
+ changed = changed || warningComment.changed;
960
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
961
+ }
962
+ } catch (error) {
963
+ const detail = errorOf(error, 'POST_SUMMARY_VERIFY_FAILED', true);
964
+ const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', detail), 'open');
965
+ receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail });
966
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
967
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
968
+ receipt = warningComment.receipt;
969
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
970
+ changed = changed || warningComment.changed;
971
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
972
+ }
973
+ }
759
974
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
760
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed);
975
+ if (receipt.summary !== 'pending' && receipt.postSummaryVerification !== 'pending' && receipt.warningProjection === 'done' && openWarnings(receipt).length === 0) {
976
+ const resolved = resolveTaskRef(taskId, { repoRoot });
977
+ if (resolved.ok && resolved.state === 'completed') clearDeliverySummary(resolved.taskDir);
978
+ }
979
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
761
980
  }
762
981
 
763
982
  async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
@@ -781,7 +1000,8 @@ export {
781
1000
  applyFinalizationReceiptMutation,
782
1001
  applyTaskFinalization,
783
1002
  issueCapability as createFinalizationCapability,
784
- readTaskFinalizationReceipt
1003
+ readTaskFinalizationReceipt,
1004
+ terminalResult
785
1005
  };
786
1006
  export type {
787
1007
  FinalizationError,
@@ -0,0 +1,133 @@
1
+ import type { VerificationStatus } from './verification-types.ts';
2
+
3
+ export type GateClassification = 'hard' | 'soft' | 'info';
4
+ export type PlatformAuditId =
5
+ | 'closed-status-labels' | 'status-label' | 'comment-marker' | 'pr-comment-marker'
6
+ | 'pr-comment-last-commit' | 'pr-comment-content' | 'comment-content' | 'task-comment-content'
7
+ | 'in-labels-computed' | 'pr-type-label' | 'in-labels-match-pr' | 'pr-assignee'
8
+ | 'requirements' | 'issue-type' | 'issue-fields' | 'milestone';
9
+
10
+ export type PlatformAuditPolicy = {
11
+ classification: GateClassification;
12
+ enabled: boolean;
13
+ expectedStatusLabel?: string;
14
+ expectedCommentMarkerKey?: 'artifact' | 'summary';
15
+ expectedPrCommentMarkerKey?: 'prSummary';
16
+ requireSpecificMilestone?: boolean;
17
+ };
18
+
19
+ export type VerificationRecord = {
20
+ type: string;
21
+ status: VerificationStatus;
22
+ message: string;
23
+ fail_type?: string;
24
+ checkId?: string;
25
+ classification?: GateClassification;
26
+ effectiveStatus?: VerificationStatus;
27
+ reason?: string;
28
+ action?: string;
29
+ dependsOn?: string;
30
+ warnings?: string[];
31
+ subchecks?: VerificationRecord[];
32
+ };
33
+
34
+ const OPTIONAL_PLATFORM_AUDITS = new Set<PlatformAuditId>([
35
+ 'closed-status-labels', 'status-label', 'comment-marker', 'pr-comment-marker',
36
+ 'pr-comment-last-commit', 'pr-comment-content', 'comment-content', 'task-comment-content',
37
+ 'in-labels-computed', 'pr-type-label', 'in-labels-match-pr', 'pr-assignee',
38
+ 'requirements', 'issue-type', 'issue-fields', 'milestone'
39
+ ]);
40
+
41
+ const STATUS_LABELS: Record<string, string> = {
42
+ 'create-task': 'status: waiting-for-triage',
43
+ 'analyze-task': 'status: pending-design-work',
44
+ 'review-analysis': 'status: pending-design-work',
45
+ 'plan-task': 'status: pending-design-work',
46
+ 'review-plan': 'status: pending-design-work',
47
+ 'code-task': 'status: in-progress',
48
+ 'review-code': 'status: in-progress'
49
+ };
50
+
51
+ const ARTIFACT_COMMENT_SKILLS = new Set([
52
+ 'analyze-task', 'plan-task', 'review-analysis', 'review-plan', 'code-task',
53
+ 'review-code', 'review-pr', 'run-manual-validation'
54
+ ]);
55
+ const PR_AUDIT_SKILLS = new Set(['create-pr']);
56
+ const IN_LABEL_AUDIT_SKILLS = new Set(['code-task', 'commit', 'create-pr']);
57
+
58
+ export function platformAuditPolicy(checkId: PlatformAuditId, context: { skillName?: string; artifactFile?: string | null }): PlatformAuditPolicy {
59
+ const skillName = context.skillName || '';
60
+ const artifactComment = Boolean(context.artifactFile) && ARTIFACT_COMMENT_SKILLS.has(skillName);
61
+ const prAudit = PR_AUDIT_SKILLS.has(skillName);
62
+ const inLabelAudit = IN_LABEL_AUDIT_SKILLS.has(skillName);
63
+
64
+ switch (checkId) {
65
+ case 'status-label':
66
+ return { classification: 'soft', enabled: Boolean(STATUS_LABELS[skillName]), expectedStatusLabel: STATUS_LABELS[skillName] };
67
+ case 'comment-marker':
68
+ return {
69
+ classification: 'soft',
70
+ enabled: artifactComment || skillName === 'complete-task',
71
+ ...(artifactComment ? { expectedCommentMarkerKey: 'artifact' } : skillName === 'complete-task' ? { expectedCommentMarkerKey: 'summary' } : {})
72
+ };
73
+ case 'pr-comment-marker':
74
+ return {
75
+ classification: 'soft',
76
+ enabled: skillName === 'commit' || prAudit,
77
+ ...(skillName === 'commit' || prAudit ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
78
+ };
79
+ case 'pr-comment-last-commit':
80
+ return {
81
+ classification: 'soft',
82
+ enabled: skillName === 'commit',
83
+ ...(skillName === 'commit' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
84
+ };
85
+ case 'pr-comment-content':
86
+ return {
87
+ classification: 'soft',
88
+ enabled: skillName === 'complete-manual-validation',
89
+ ...(skillName === 'complete-manual-validation' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
90
+ };
91
+ case 'comment-content':
92
+ return { classification: 'soft', enabled: artifactComment, expectedCommentMarkerKey: 'artifact' };
93
+ case 'task-comment-content':
94
+ return { classification: 'soft', enabled: true };
95
+ case 'in-labels-computed':
96
+ return { classification: 'soft', enabled: inLabelAudit };
97
+ case 'pr-type-label':
98
+ case 'in-labels-match-pr':
99
+ case 'pr-assignee':
100
+ return { classification: 'soft', enabled: prAudit };
101
+ case 'requirements':
102
+ return { classification: 'soft', enabled: skillName === 'complete-task' };
103
+ case 'milestone':
104
+ return { classification: 'soft', enabled: true, requireSpecificMilestone: skillName === 'code-task' };
105
+ case 'closed-status-labels':
106
+ case 'issue-type':
107
+ case 'issue-fields':
108
+ return { classification: 'soft', enabled: true };
109
+ }
110
+ }
111
+
112
+ export function classificationForCheck(checkId: string): GateClassification {
113
+ return checkId.startsWith('platform.') && OPTIONAL_PLATFORM_AUDITS.has(checkId.slice('platform.'.length) as PlatformAuditId)
114
+ ? 'soft'
115
+ : 'hard';
116
+ }
117
+
118
+ export function effectiveStatus(status: VerificationStatus, classification: GateClassification): VerificationStatus {
119
+ return classification === 'hard' ? status : 'pass';
120
+ }
121
+
122
+ export function normalizeVerificationRecord(record: VerificationRecord): VerificationRecord {
123
+ const checkId = record.checkId ?? record.type;
124
+ const classification = record.classification ?? classificationForCheck(checkId);
125
+ return {
126
+ ...record,
127
+ checkId,
128
+ classification,
129
+ effectiveStatus: record.effectiveStatus ?? effectiveStatus(record.status, classification),
130
+ reason: record.reason ?? record.fail_type ?? (classification === 'info' ? 'NOT_APPLICABLE' : record.status === 'pass' ? 'OK' : 'CHECK_FAILED'),
131
+ action: record.action ?? (classification === 'info' || record.status === 'pass' ? 'No action required' : 'Review the check evidence and retry after correction')
132
+ };
133
+ }