@fitlab-ai/agent-infra 0.10.0 → 0.11.1

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 (154) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/internal/cli-route-inventory.js +3 -4
  6. package/dist/lib/internal/manual-validation.js +6 -2
  7. package/dist/lib/internal/platform-comment.js +60 -2
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-finalization.js +2 -2
  10. package/dist/lib/internal/task-lifecycle.js +2 -2
  11. package/dist/lib/internal/task-operation-registry.js +22 -12
  12. package/dist/lib/internal/task-orchestration.js +2 -2
  13. package/dist/lib/internal/task-review.js +10 -5
  14. package/dist/lib/platform/activity-recovery.js +159 -0
  15. package/dist/lib/platform/issue-comments.js +84 -32
  16. package/dist/lib/platform/issues.js +3 -4
  17. package/dist/lib/platform/task-comment-projection.js +70 -0
  18. package/dist/lib/platform/task-recovery.js +55 -0
  19. package/dist/lib/platform/verification-sync.js +184 -162
  20. package/dist/lib/sandbox/control/executor.js +4 -2
  21. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  22. package/dist/lib/sandbox/control/workflow-executor.js +90 -22
  23. package/dist/lib/server/process-state.js +17 -5
  24. package/dist/lib/task/artifact-command.js +6 -3
  25. package/dist/lib/task/artifact-lifecycle.js +15 -52
  26. package/dist/lib/task/artifact-operations.js +28 -2
  27. package/dist/lib/task/artifact-recovery.js +114 -68
  28. package/dist/lib/task/artifact-repair-intent.js +25 -8
  29. package/dist/lib/task/capabilities.js +0 -2
  30. package/dist/lib/task/control-authority.js +1 -1
  31. package/dist/lib/task/events.js +66 -49
  32. package/dist/lib/task/gate-policy.js +94 -0
  33. package/dist/lib/task/intermediate-cleanup.js +28 -3
  34. package/dist/lib/task/local-artifact-finalization.js +85 -18
  35. package/dist/lib/task/orchestration.js +8 -6
  36. package/dist/lib/task/review-artifacts.js +12 -6
  37. package/dist/lib/task/review-command.js +2 -2
  38. package/dist/lib/task/review-finalization.js +198 -11
  39. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  40. package/dist/lib/task/verification-engine.js +28 -17
  41. package/dist/lib/task/verification.js +25 -9
  42. package/dist/lib/task/workflow-command.js +2 -0
  43. package/dist/lib/task/workflow-dispatch.js +161 -0
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/internal/cli-route-inventory.ts +3 -4
  46. package/lib/internal/manual-validation.ts +6 -2
  47. package/lib/internal/platform-comment.ts +53 -2
  48. package/lib/internal/task-artifact.ts +1 -1
  49. package/lib/internal/task-finalization.ts +1 -1
  50. package/lib/internal/task-lifecycle.ts +1 -1
  51. package/lib/internal/task-operation-registry.ts +22 -12
  52. package/lib/internal/task-orchestration.ts +1 -1
  53. package/lib/internal/task-review.ts +9 -5
  54. package/lib/platform/activity-recovery.ts +179 -0
  55. package/lib/platform/issue-comments.ts +98 -31
  56. package/lib/platform/issues.ts +3 -4
  57. package/lib/platform/task-comment-projection.ts +79 -0
  58. package/lib/platform/task-recovery.ts +54 -0
  59. package/lib/platform/verification-sync.ts +203 -188
  60. package/lib/sandbox/control/executor.ts +4 -2
  61. package/lib/sandbox/control/protocol.ts +1 -1
  62. package/lib/sandbox/control/task-workflow.ts +1 -1
  63. package/lib/sandbox/control/workflow-executor.ts +113 -22
  64. package/lib/server/process-state.ts +26 -5
  65. package/lib/task/artifact-command.ts +6 -4
  66. package/lib/task/artifact-lifecycle.ts +12 -49
  67. package/lib/task/artifact-operations.ts +28 -2
  68. package/lib/task/artifact-recovery.ts +120 -67
  69. package/lib/task/artifact-repair-intent.ts +38 -11
  70. package/lib/task/capabilities.ts +0 -1
  71. package/lib/task/control-authority.ts +1 -1
  72. package/lib/task/events.ts +55 -38
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/intermediate-cleanup.ts +18 -2
  75. package/lib/task/local-artifact-finalization.ts +91 -15
  76. package/lib/task/orchestration.ts +12 -7
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +1 -1
  79. package/lib/task/review-finalization.ts +198 -9
  80. package/lib/{host-control → task}/secure-fs.ts +10 -32
  81. package/lib/task/verification-engine.ts +24 -14
  82. package/lib/task/verification-types.ts +7 -0
  83. package/lib/task/verification.ts +25 -9
  84. package/lib/task/workflow-command.ts +2 -0
  85. package/lib/task/workflow-dispatch.ts +171 -0
  86. package/package.json +1 -1
  87. package/templates/.agents/rules/issue-sync.en.md +11 -1
  88. package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
  89. package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
  90. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
  91. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  94. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  95. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  96. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  97. package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
  98. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
  99. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  100. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  101. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  102. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  104. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  105. package/templates/.agents/skills/commit/config/verify.json +1 -5
  106. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  108. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  109. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  110. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  111. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  112. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  113. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  116. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  117. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  118. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
  119. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  120. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  121. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  122. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  123. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  124. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  125. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  126. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  127. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  128. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  129. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  130. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  131. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  134. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  135. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  136. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  137. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  138. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  139. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  140. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  141. package/dist/lib/host-control/audit.js +0 -19
  142. package/dist/lib/host-control/client.js +0 -142
  143. package/dist/lib/host-control/command.js +0 -49
  144. package/dist/lib/host-control/path.js +0 -160
  145. package/dist/lib/host-control/server.js +0 -159
  146. package/dist/lib/host-control/service.js +0 -112
  147. package/dist/lib/internal/host-control.js +0 -28
  148. package/lib/host-control/audit.ts +0 -22
  149. package/lib/host-control/client.ts +0 -172
  150. package/lib/host-control/command.ts +0 -60
  151. package/lib/host-control/path.ts +0 -167
  152. package/lib/host-control/server.ts +0 -180
  153. package/lib/host-control/service.ts +0 -133
  154. package/lib/internal/host-control.ts +0 -28
@@ -20,7 +20,7 @@ import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead,
20
20
  import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.js';
21
21
  import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.js';
22
22
  import { hasOpenLifecycleExecution } from './activity-log.js';
23
- import { invalidationBlocks } from './invalidation.js';
23
+ import { reconcileTaskInvalidation } from './invalidation-command.js';
24
24
  import { normalizeAgentToken } from '../agent-clients/tokens.js';
25
25
  function supportsLifecycleDelegation(client) {
26
26
  return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
@@ -511,14 +511,12 @@ function routeOrchestration(taskRef, options = {}) {
511
511
  }
512
512
  let metadata;
513
513
  let currentLedger = null;
514
- let currentInvalidation = { operations: [], targets: [] };
515
514
  if (resolved.state === 'active') {
516
515
  const contract = validateCurrentTaskContract(content);
517
516
  if (!contract.ok)
518
517
  return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
519
518
  metadata = contract.metadata;
520
519
  currentLedger = contract.ledger;
521
- currentInvalidation = contract.invalidation;
522
520
  }
523
521
  else {
524
522
  try {
@@ -528,9 +526,6 @@ function routeOrchestration(taskRef, options = {}) {
528
526
  return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
529
527
  }
530
528
  }
531
- if (invalidationBlocks(currentInvalidation)) {
532
- return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
533
- }
534
529
  let run;
535
530
  try {
536
531
  run = readRun(resolved.taskDir, options);
@@ -545,6 +540,9 @@ function routeOrchestration(taskRef, options = {}) {
545
540
  if (!routed)
546
541
  return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
547
542
  if ('completion' in routed) {
543
+ if (facts.facts.executionBusy && !run?.pendingDelegation) {
544
+ return failed('ORCHESTRATION_EXECUTION_BUSY', 'clean completion requires no open lifecycle execution', resolved.taskId);
545
+ }
548
546
  const reviewRound = maxArtifactRound(fs.readdirSync(resolved.taskDir), 'review-code');
549
547
  const review = parseReviewSummary(fs.readFileSync(path.join(resolved.taskDir, artifactName('review-code', reviewRound)), 'utf8'));
550
548
  if (!review.ok || review.summary.manualValidation === null) {
@@ -670,6 +668,10 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
670
668
  }
671
669
  if (run.stepCount >= run.maxSteps)
672
670
  return pauseOrchestration(taskRef, 'ORCHESTRATION_MAX_STEPS', 'maximum orchestration steps reached', true, options);
671
+ const invalidation = reconcileTaskInvalidation(taskRef, { repoRoot: resolved.repoRoot });
672
+ if (invalidation.status === 'failed') {
673
+ return failed('ORCHESTRATION_INVALIDATION_RECONCILE_FAILED', `${invalidation.error?.code ?? 'UNKNOWN'}: ${invalidation.error?.message ?? ''}`, resolved.taskId);
674
+ }
673
675
  const routed = routeOrchestration(taskRef, options);
674
676
  if (!routed.next)
675
677
  return routed;
@@ -118,20 +118,26 @@ function parseReviewSummary(content) {
118
118
  function equalCounts(left, right) {
119
119
  return left.blocker === right.blocker && left.major === right.major && left.minor === right.minor;
120
120
  }
121
- function finalizeReviewSummaryContent(content, counts) {
121
+ function finalizeReviewSummaryContent(content, counts, options = {}) {
122
122
  const parsed = parseReviewSummary(content);
123
123
  if (!parsed.ok)
124
124
  return parsed;
125
125
  if (parsed.summary.countState === 'numeric') {
126
126
  if (equalCounts(parsed.summary.counts, counts))
127
127
  return { ok: true, changed: false, content };
128
- return {
129
- ok: false,
130
- code: 'REVIEW_SUMMARY_COUNT_MISMATCH',
131
- message: 'review summary finding counts do not match the ledger snapshot'
132
- };
128
+ if (!options.refreshNumericCounts)
129
+ return {
130
+ ok: false,
131
+ code: 'REVIEW_SUMMARY_COUNT_MISMATCH',
132
+ message: 'review summary finding counts do not match the ledger snapshot'
133
+ };
133
134
  }
134
135
  let line = content.slice(parsed.findingsStart, parsed.findingsEnd);
136
+ if (parsed.summary.countState === 'numeric') {
137
+ const values = [counts.blocker, counts.major, counts.minor];
138
+ let ordinal = 0;
139
+ line = line.replace(/\d+(?=\s*(?:阻塞项|主要|次要|blockers?\b|majors?\b|minors?\b))/gi, () => String(values[ordinal++]));
140
+ }
135
141
  line = line
136
142
  .replace(PLACEHOLDERS[0], String(counts.blocker))
137
143
  .replace(PLACEHOLDERS[1], String(counts.major))
@@ -1,6 +1,6 @@
1
1
  export function parseReviewCommand(args) {
2
- if (!args[0] || args[1] !== 'finalize-summary')
3
- throw new Error('task ref and finalize-summary are required');
2
+ if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? ''))
3
+ throw new Error('task ref and a supported review operation are required');
4
4
  const values = {};
5
5
  const flags = {
6
6
  '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
@@ -10,8 +10,9 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
10
10
  import { allowsManualOverride } from './guard-override.js';
11
11
  import { getArtifactSchema } from './artifact-schema.js';
12
12
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
13
+ import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
13
14
  import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
14
- import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryCommit, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
15
+ import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
15
16
  const STAGES = {
16
17
  analysis: { family: 'review-analysis' },
17
18
  plan: { family: 'review-plan' },
@@ -37,6 +38,154 @@ function failed(request, code, message, taskId = null, stageStatus = null, extra
37
38
  ...extra
38
39
  };
39
40
  }
41
+ function preflightFailed(request, code, message, taskId = null, extra = {}) {
42
+ const { status: _status, intent: _intent, stageStatus: _stageStatus, ...finalizationExtra } = extra;
43
+ return {
44
+ ...failed(request, code, message, taskId, null, finalizationExtra),
45
+ status: 'failed',
46
+ intent: 'preflight',
47
+ stageStatus: null
48
+ };
49
+ }
50
+ /** Validate and seal a review draft before any finding-ledger write. */
51
+ function preflightReviewSummaryUnlocked(request, options = {}) {
52
+ const stage = request.stage;
53
+ const spec = STAGES[stage];
54
+ if (!spec)
55
+ return preflightFailed(request, 'REVIEW_STAGE_INVALID', `unsupported review stage '${request.stage}'`);
56
+ if (!request.taskRef || !request.artifact)
57
+ return preflightFailed(request, 'REVIEW_PAYLOAD_INVALID', 'taskRef, stage, and artifact are required');
58
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
59
+ if (!resolved.ok)
60
+ return preflightFailed(request, resolved.code, resolved.message, resolved.taskId ?? null);
61
+ if (resolved.state !== 'active')
62
+ return preflightFailed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
63
+ const parsed = parseArtifactName(request.artifact);
64
+ if (!parsed || parsed.family !== spec.family)
65
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${spec.family}`, resolved.taskId);
66
+ const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
67
+ if (publicationError)
68
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
69
+ let taskContent;
70
+ let content;
71
+ let recovery;
72
+ try {
73
+ taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
74
+ if (request.recoveryId) {
75
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
76
+ if (!intent || intent.recoveryOperationId !== request.recoveryId)
77
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
78
+ if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state))
79
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
80
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
81
+ content = fs.existsSync(recovery.stagingPath)
82
+ ? fs.readFileSync(recovery.stagingPath, 'utf8')
83
+ : fs.readFileSync(recovery.formalPath, 'utf8');
84
+ }
85
+ else {
86
+ const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
87
+ if (!validated.ok)
88
+ return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
89
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
90
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
91
+ if (intent?.state === 'preflight-ready')
92
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
93
+ if (intent?.state === 'consumed')
94
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
95
+ }
96
+ }
97
+ catch (error) {
98
+ return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId);
99
+ }
100
+ if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round))
101
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
102
+ const execution = validateLifecycleExecution(request.taskRef, {
103
+ mode: request.orchestrated ? 'orchestrated' : 'standalone',
104
+ identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
105
+ dryRun: request.dryRun
106
+ }, { repoRoot: options.repoRoot });
107
+ if (!execution.ok)
108
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${execution.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH'}: ${execution.error?.message ?? 'orchestration provenance validation failed'}`, resolved.taskId);
109
+ const artifactSha256 = sha256Content(content);
110
+ const semanticDigest = canonicalSemanticDigest(content);
111
+ const detail = inspectDecisionDetailDuplicates(content);
112
+ const structure = inspectArtifactContract(content, getArtifactSchema(spec.family));
113
+ const expected = expectedQualificationRelations(taskContent, spec.family);
114
+ if (!expected.ok)
115
+ return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
116
+ const qualification = validateQualificationAudit(taskContent, content, {
117
+ family: spec.family,
118
+ artifact: request.artifact,
119
+ expectedUpstreamRelations: expected.relations
120
+ });
121
+ const qualificationError = qualification.ok ? null : qualification;
122
+ if (!detail.ok || !structure.ok || qualificationError) {
123
+ if (!recovery && !request.dryRun) {
124
+ try {
125
+ recovery = beginArtifactRecovery({
126
+ taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
127
+ requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
128
+ }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
129
+ }
130
+ catch (error) {
131
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId);
132
+ }
133
+ }
134
+ const diagnostics = [
135
+ ...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
136
+ ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
137
+ ...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
138
+ ].join('; ');
139
+ return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
140
+ artifactSha256, semanticDigest,
141
+ ...(recovery ? { recovery: recoveryInfo(recovery) } : {})
142
+ });
143
+ }
144
+ if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
145
+ return {
146
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
147
+ status: 'passed', changed: false, error: null
148
+ };
149
+ }
150
+ try {
151
+ recovery ??= beginArtifactRecovery({
152
+ taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
153
+ requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
154
+ }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
155
+ const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
156
+ prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
157
+ return {
158
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
159
+ status: 'passed', changed: false, error: null
160
+ };
161
+ }
162
+ catch (error) {
163
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest });
164
+ }
165
+ }
166
+ function preflightReviewSummary(request, options = {}) {
167
+ if (request.dryRun || options.lockAlreadyHeld)
168
+ return preflightReviewSummaryUnlocked(request, options);
169
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
170
+ if (!resolved.ok)
171
+ return preflightReviewSummaryUnlocked(request, options);
172
+ try {
173
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.preflight', () => preflightReviewSummaryUnlocked(request, { ...options, lockAlreadyHeld: true }));
174
+ }
175
+ catch (error) {
176
+ if (!(error instanceof TaskExecutionLockError))
177
+ throw error;
178
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
179
+ }
180
+ }
181
+ function recoveryInfo(recovery) {
182
+ return {
183
+ recoveryId: recovery.recoveryId,
184
+ candidatePath: recovery.stagingPath,
185
+ baselineSha256: recovery.baselineSha256,
186
+ baselineSemanticDigest: recovery.baselineSemanticDigest
187
+ };
188
+ }
40
189
  /** Prepare domain validation, summary bytes and provenance before any publication. */
41
190
  function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
42
191
  let taskId = null;
@@ -76,7 +225,7 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
76
225
  const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
77
226
  if (!intent || intent.recoveryOperationId !== request.recoveryId)
78
227
  return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
79
- if (!['awaiting-recovery', 'finalize-ready', 'commit-started'].includes(intent.state))
228
+ if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
80
229
  return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
81
230
  recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
82
231
  artifactContent = fs.existsSync(recovery.stagingPath)
@@ -97,11 +246,15 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
97
246
  catch (error) {
98
247
  return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
99
248
  }
100
- if (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed') {
249
+ if (repairIntent?.state === 'consumed') {
101
250
  if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
102
251
  return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
103
252
  }
104
253
  }
254
+ if (repairIntent?.state === 'passed'
255
+ && (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
256
+ repairIntent = undefined;
257
+ }
105
258
  if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
106
259
  return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
107
260
  }
@@ -155,10 +308,18 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
155
308
  ...(recovery ? { recovery } : {})
156
309
  };
157
310
  }
158
- const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts);
311
+ const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
312
+ refreshNumericCounts: Boolean(request.recoveryId && recovery)
313
+ });
159
314
  if (!transformed.ok)
160
315
  return reject(transformed.code, transformed.message);
161
316
  const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
317
+ if (!recovery && repairIntent?.state === 'commit-started') {
318
+ if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
319
+ return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
320
+ }
321
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
322
+ }
162
323
  if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
163
324
  if (transformed.changed)
164
325
  return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
@@ -197,7 +358,7 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
197
358
  lockAlreadyHeld: options.lockAlreadyHeld
198
359
  };
199
360
  }
200
- function commitReviewSummaryProvenance(prepared, repoRoot) {
361
+ function commitReviewSummaryProvenance(prepared, repoRoot, options = {}) {
201
362
  if (prepared.result.status === 'failed' || !prepared.recovery)
202
363
  return prepared.result;
203
364
  try {
@@ -205,16 +366,40 @@ function commitReviewSummaryProvenance(prepared, repoRoot) {
205
366
  if (!intent)
206
367
  throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
207
368
  let committed;
208
- if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
209
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
369
+ if (intent.state === 'passed' || intent.state === 'consumed') {
370
+ return {
371
+ ...prepared.result,
372
+ status: 'no-op',
373
+ changed: false,
374
+ artifactSha256: intent.finalArtifactSha256,
375
+ semanticDigest: intent.finalSemanticDigest,
376
+ error: null
377
+ };
210
378
  }
211
- else {
379
+ else if (intent.state === 'commit-started') {
380
+ const reconciled = reconcileArtifactRecovery(prepared.recovery, {
381
+ lockAlreadyHeld: prepared.lockAlreadyHeld,
382
+ afterPublish: options.afterPublish
383
+ });
384
+ committed = reconciled.intent;
385
+ }
386
+ else if (intent.state === 'awaiting-preflight-recovery') {
212
387
  const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
213
388
  prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
214
389
  committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
215
390
  }
216
- if (committed.state !== 'passed')
217
- throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
391
+ else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
392
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
393
+ }
394
+ if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
395
+ prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
396
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
397
+ }
398
+ else if (intent.state === 'full-finalizer-ready') {
399
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
400
+ }
401
+ if (!committed || committed.state !== 'passed')
402
+ throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
218
403
  return {
219
404
  ...prepared.result,
220
405
  artifactSha256: committed.finalArtifactSha256,
@@ -223,6 +408,8 @@ function commitReviewSummaryProvenance(prepared, repoRoot) {
223
408
  };
224
409
  }
225
410
  catch (error) {
411
+ if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
412
+ throw error;
226
413
  return { ...prepared.result, status: 'failed', error: {
227
414
  code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
228
415
  } };
@@ -268,5 +455,5 @@ function finalizeReviewSummary(request, options = {}) {
268
455
  return failed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
269
456
  }
270
457
  }
271
- export { finalizeReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
458
+ export { finalizeReviewSummary, preflightReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
272
459
  //# sourceMappingURL=review-finalization.js.map
@@ -1,6 +1,6 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
4
  const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
5
5
  const DIRECTORY = fs.constants.O_DIRECTORY ?? 0;
6
6
  export class SecureFileError extends Error {
@@ -15,11 +15,8 @@ function conflict(message) {
15
15
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', message);
16
16
  }
17
17
  function sameStat(left, right) {
18
- return left.dev === right.dev
19
- && left.ino === right.ino
20
- && left.size === right.size
21
- && left.mtimeNs === right.mtimeNs
22
- && left.ctimeNs === right.ctimeNs;
18
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size
19
+ && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
23
20
  }
24
21
  function assertNoSymlinkAncestors(candidate) {
25
22
  let current = path.dirname(candidate);
@@ -36,15 +33,12 @@ function assertNoSymlinkAncestors(candidate) {
36
33
  export async function readStableFile(candidate, options) {
37
34
  return readStableFileSync(candidate, options);
38
35
  }
39
- /** Shared bounded reader for synchronous domain commands and async workflow callers. */
40
36
  export function readStableFileSync(candidate, options) {
41
- if (!path.isAbsolute(candidate)) {
37
+ if (!path.isAbsolute(candidate))
42
38
  conflict('candidate path must be absolute and terminal');
43
- }
44
39
  assertNoSymlinkAncestors(candidate);
45
40
  let descriptor;
46
41
  try {
47
- // O_NONBLOCK lets fstat reject a FIFO even when it has no writer.
48
42
  descriptor = fs.openSync(candidate, fs.constants.O_RDONLY | NO_FOLLOW | (fs.constants.O_NONBLOCK ?? 0));
49
43
  const before = fs.fstatSync(descriptor, { bigint: true });
50
44
  if (!before.isFile())
@@ -63,18 +57,16 @@ export function readStableFileSync(candidate, options) {
63
57
  if (!sameStat(before, after))
64
58
  conflict('candidate identity or metadata changed during read');
65
59
  const sha256 = createHash('sha256').update(bytes).digest('hex');
66
- if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256) {
60
+ if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256)
67
61
  conflict('candidate digest does not match the expected digest');
68
- }
69
62
  return { bytes, sha256, stat: after };
70
63
  }
71
64
  catch (error) {
72
65
  if (error instanceof SecureFileError)
73
66
  throw error;
74
67
  const code = error.code;
75
- if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL') {
68
+ if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL')
76
69
  conflict('candidate terminal is not a stable non-symlink file');
77
- }
78
70
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', error instanceof Error ? error.message : String(error));
79
71
  }
80
72
  finally {
@@ -93,9 +85,8 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
93
85
  catch {
94
86
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is missing');
95
87
  }
96
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
88
+ if (!stat.isDirectory() || stat.isSymbolicLink())
97
89
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is not a real directory');
98
- }
99
90
  const next = path.dirname(current);
100
91
  if (next === current)
101
92
  break;
@@ -103,9 +94,8 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
103
94
  }
104
95
  try {
105
96
  const existing = await fs.promises.lstat(target);
106
- if (existing.isSymbolicLink() || !existing.isFile()) {
97
+ if (existing.isSymbolicLink() || !existing.isFile())
107
98
  throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative target is not a regular file');
108
- }
109
99
  }
110
100
  catch (error) {
111
101
  if (error.code !== 'ENOENT')
@@ -120,7 +110,6 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
120
110
  await handle.chmod(mode);
121
111
  await handle.close();
122
112
  handle = undefined;
123
- // Check after all asynchronous preparation; keep the final check/rename adjacent.
124
113
  if (expectedSha256 !== undefined)
125
114
  readStableFileSync(target, { maxBytes: 1024 * 1024, expectedSha256 });
126
115
  fs.renameSync(temporary, target);
@@ -29,6 +29,7 @@ import { validateLocalArtifact } from "./local-artifact-finalization.js";
29
29
  import { validateQualificationAudit } from "./qualification-audit.js";
30
30
  import { getArtifactSchema } from "./artifact-schema.js";
31
31
  import { inspectArtifactContract } from "./artifact-operations.js";
32
+ import { normalizeVerificationRecord } from './gate-policy.js';
32
33
  import { manualValidationFinalSummaryProjectionMatches } from "./manual-validation-receipt.js";
33
34
  import { readManualValidationCompletion } from "./manual-validation-completion.js";
34
35
  import { sha256File } from "./artifact-receipts.js";
@@ -1198,19 +1199,19 @@ function interpolate(template, taskDir, artifactFile) {
1198
1199
  .replace(/\{artifact-stem\}/g, artifactStem);
1199
1200
  }
1200
1201
  function summarizeGate(checks) {
1201
- if (checks.some((check) => check.status === "blocked")) {
1202
+ if (checks.some((check) => check.effectiveStatus === "blocked")) {
1202
1203
  return "blocked";
1203
1204
  }
1204
- if (checks.some((check) => check.status === "fail")) {
1205
+ if (checks.some((check) => check.effectiveStatus === "fail")) {
1205
1206
  return "fail";
1206
1207
  }
1207
1208
  return "pass";
1208
1209
  }
1209
1210
  function summarizeChecks(checks) {
1210
1211
  const counts = {
1211
- pass: checks.filter((check) => check.status === "pass").length,
1212
- fail: checks.filter((check) => check.status === "fail").length,
1213
- blocked: checks.filter((check) => check.status === "blocked").length
1212
+ pass: checks.filter((check) => check.effectiveStatus === "pass").length,
1213
+ fail: checks.filter((check) => check.effectiveStatus === "fail").length,
1214
+ blocked: checks.filter((check) => check.effectiveStatus === "blocked").length
1214
1215
  };
1215
1216
  if (counts.blocked > 0) {
1216
1217
  return `${counts.pass} passed, ${counts.fail} failed, ${counts.blocked} blocked`;
@@ -1221,7 +1222,7 @@ function buildAction(gate, checks) {
1221
1222
  if (gate === "pass") {
1222
1223
  return "All declared checks passed";
1223
1224
  }
1224
- const firstFailure = checks.find((check) => check.status !== "pass");
1225
+ const firstFailure = checks.find((check) => check.effectiveStatus !== "pass");
1225
1226
  if (!firstFailure) {
1226
1227
  return "Review validation output";
1227
1228
  }
@@ -1277,16 +1278,16 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
1277
1278
  for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
1278
1279
  if (checkConfig === null)
1279
1280
  continue;
1280
- const result = await runCheck(type, {
1281
+ const result = normalizeVerificationRecord(await runCheck(type, {
1281
1282
  mode,
1282
1283
  skillName,
1283
1284
  taskDir: path.resolve(taskDir),
1284
1285
  artifactFile,
1285
1286
  repositoryRoot: repoRoot,
1286
1287
  config: checkConfig
1287
- }, shared);
1288
- checks.push(result);
1289
- if (result.status === "blocked")
1288
+ }, shared));
1289
+ checks.push(...(result.subchecks?.map(normalizeVerificationRecord) ?? [result]));
1290
+ if (checks.at(-1)?.effectiveStatus === "blocked")
1290
1291
  break;
1291
1292
  }
1292
1293
  const gate = summarizeGate(checks);
@@ -1298,15 +1299,25 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
1298
1299
  return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
1299
1300
  if (config === null)
1300
1301
  return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
1302
+ const result = normalizeVerificationRecord(await runCheck(type, {
1303
+ skillName,
1304
+ taskDir: path.resolve(taskDir),
1305
+ artifactFile,
1306
+ repositoryRoot: repoRoot,
1307
+ config
1308
+ }, shared));
1309
+ if (!result.subchecks?.length)
1310
+ return { skill: skillName, ...result };
1311
+ const checks = result.subchecks.map(normalizeVerificationRecord);
1312
+ const gate = summarizeGate(checks);
1301
1313
  return {
1302
1314
  skill: skillName,
1303
- ...await runCheck(type, {
1304
- skillName,
1305
- taskDir: path.resolve(taskDir),
1306
- artifactFile,
1307
- repositoryRoot: repoRoot,
1308
- config
1309
- }, shared)
1315
+ ...result,
1316
+ status: gate,
1317
+ effectiveStatus: gate,
1318
+ reason: gate === 'pass' ? 'OK' : checks.find((check) => check.effectiveStatus !== 'pass')?.reason ?? 'CHECK_FAILED',
1319
+ action: buildAction(gate, checks),
1320
+ checks
1310
1321
  };
1311
1322
  }
1312
1323
  export { verifyInProcess };
@@ -80,7 +80,7 @@ async function verifyTaskEvent(request, options = {}) {
80
80
  else {
81
81
  for (const check of spec.checks ?? []) {
82
82
  const payload = await engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
83
- const status = payload.status;
83
+ const status = (payload.effectiveStatus ?? payload.status);
84
84
  if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
85
85
  invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
86
86
  break;
@@ -103,19 +103,35 @@ function statusLabel(status) {
103
103
  function renderPayload(payload) {
104
104
  const lines = [];
105
105
  if (Array.isArray(payload.checks)) {
106
- lines.push(`Verification: ${payload.gate} | Skill: ${payload.skill}`, '');
106
+ const gateMode = typeof payload.gate === 'string';
107
+ lines.push(gateMode
108
+ ? `Verification: ${payload.gate} | Skill: ${payload.skill}`
109
+ : `Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
107
110
  for (const check of payload.checks) {
108
- lines.push(` [${statusLabel(check.status)}] ${check.type} - ${check.message}`);
111
+ const raw = statusLabel(check.status);
112
+ const effective = statusLabel(check.effectiveStatus ?? check.status);
113
+ const reason = check.reason ?? 'CHECK_FAILED';
114
+ const message = check.message ?? reason;
115
+ lines.push(` [${effective}] ${check.checkId ?? check.type} (${check.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${message}; ${check.action ?? 'Review validation output'}`);
109
116
  }
110
- lines.push('', `Result: ${payload.summary} - ${payload.action}`);
117
+ const summary = payload.summary ?? (() => {
118
+ const checks = payload.checks;
119
+ const statuses = checks.map((check) => check.effectiveStatus ?? check.status);
120
+ return `${statuses.filter((status) => status === 'pass').length} passed, ${statuses.filter((status) => status === 'fail').length} failed${statuses.includes('blocked') ? `, ${statuses.filter((status) => status === 'blocked').length} blocked` : ''}`;
121
+ })();
122
+ lines.push('', `Result: ${summary} - ${payload.action}`);
111
123
  }
112
124
  else {
113
125
  lines.push(`Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
114
- lines.push(` [${statusLabel(payload.status)}] ${payload.type} - ${payload.message}`);
115
- const summary = payload.status === 'pass' ? '1 passed, 0 failed'
116
- : payload.status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
117
- const action = payload.status === 'pass' ? 'Requested check passed'
118
- : payload.status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
126
+ const raw = statusLabel(payload.status);
127
+ const effective = statusLabel(payload.effectiveStatus ?? payload.status);
128
+ const reason = payload.reason ?? 'CHECK_FAILED';
129
+ lines.push(` [${effective}] ${payload.checkId ?? payload.type} (${payload.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${payload.message ?? reason}; ${payload.action ?? 'Review validation output'}`);
130
+ const status = payload.effectiveStatus ?? payload.status;
131
+ const summary = status === 'pass' ? '1 passed, 0 failed'
132
+ : status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
133
+ const action = status === 'pass' ? 'Requested check passed'
134
+ : status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
119
135
  : `Fix ${payload.type} issues and re-run check`;
120
136
  lines.push('', `Result: ${summary} - ${action}`);
121
137
  }
@@ -1,7 +1,9 @@
1
1
  export const TASK_WORKFLOW_COMMANDS = Object.freeze({
2
2
  'artifact-inspect': ['task-artifact', 'inspect'],
3
3
  'artifact-init': ['task-artifact', 'init'],
4
+ 'artifact-preflight': ['task-artifact', 'preflight'],
4
5
  'artifact-finalize-local': ['task-artifact', 'finalize-local'],
6
+ 'review-preflight': ['task-review', 'preflight'],
5
7
  'review-finalize-summary': ['task-review', 'finalize-summary'],
6
8
  event: ['task-event', null],
7
9
  'ledger-finding-response': ['task-ledger', 'finding-respond'],