@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,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
2
4
 
3
5
  import { parseArtifactName } from './artifact-name.ts';
4
- import { validateCompletedArtifact, hasOpenArtifactRound, validateArtifactPublication } from './artifact-lifecycle.ts';
6
+ import { validateCompletedArtifact, validateArtifactPublication } from './artifact-lifecycle.ts';
5
7
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
6
8
  import type { LedgerStageStatus, ReviewStage } from './ledger.ts';
7
9
  import { finalizeReviewSummaryContent } from './review-artifacts.ts';
@@ -15,17 +17,6 @@ import type { ManualOverrideCapability } from './guard-override.ts';
15
17
  import { getArtifactSchema } from './artifact-schema.ts';
16
18
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
17
19
  import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
18
- import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
19
- import {
20
- beginArtifactRecovery,
21
- commitArtifactRecovery,
22
- prepareArtifactRecoveryFinal,
23
- prepareArtifactRecoveryCommit,
24
- reconcileArtifactRecovery,
25
- recoveryContextFromIntent,
26
- stageArtifactCandidate
27
- } from './artifact-recovery.ts';
28
- import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
29
20
 
30
21
  type ReviewFinalizationErrorCode =
31
22
  | ResolveTaskRefErrorCode
@@ -51,7 +42,6 @@ type ReviewFinalizationRequest = {
51
42
  artifact: string;
52
43
  orchestrated?: boolean;
53
44
  dryRun?: boolean;
54
- recoveryId?: string;
55
45
  };
56
46
  type ReviewFinalizationResult = {
57
47
  status: 'planned' | 'applied' | 'no-op' | 'failed';
@@ -64,12 +54,6 @@ type ReviewFinalizationResult = {
64
54
  stageStatus: LedgerStageStatus | null;
65
55
  artifactSha256: string | null;
66
56
  semanticDigest: string | null;
67
- recovery?: Readonly<{
68
- recoveryId: string;
69
- candidatePath: string;
70
- baselineSha256: string;
71
- baselineSemanticDigest: string;
72
- }>;
73
57
  operations: readonly {
74
58
  kind: 'artifact';
75
59
  artifact: string;
@@ -90,7 +74,6 @@ type ReviewFinalizationOptions = {
90
74
  fileSystem?: Partial<ReviewFileSystem>;
91
75
  manualOverride?: ManualOverrideCapability;
92
76
  lockAlreadyHeld?: boolean;
93
- startRecovery?: boolean;
94
77
  };
95
78
 
96
79
  const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code' }> = {
@@ -143,7 +126,7 @@ function preflightFailed(
143
126
  };
144
127
  }
145
128
 
146
- /** Validate and seal a review draft before any finding-ledger write. */
129
+ /** Validate the current review draft before any finding-ledger write. */
147
130
  function preflightReviewSummaryUnlocked(
148
131
  request: ReviewFinalizationRequest,
149
132
  options: ReviewFinalizationOptions = {}
@@ -162,30 +145,13 @@ function preflightReviewSummaryUnlocked(
162
145
 
163
146
  let taskContent: string;
164
147
  let content: string;
165
- let recovery: ArtifactRecoveryContext | undefined;
166
- let existingIntent;
167
148
  try {
168
149
  taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
169
- if (request.recoveryId) {
170
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
171
- if (!intent || intent.recoveryOperationId !== request.recoveryId) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
172
- if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state)) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
173
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
174
- content = fs.existsSync(recovery.stagingPath)
175
- ? fs.readFileSync(recovery.stagingPath, 'utf8')
176
- : fs.readFileSync(recovery.formalPath, 'utf8');
177
- } else {
178
- const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
179
- if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
180
- content = fs.readFileSync(validated.artifact.path, 'utf8');
181
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
182
- existingIntent = intent;
183
- if (intent?.state === 'preflight-ready') recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
184
- if (intent?.state === 'consumed') return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
185
- }
150
+ const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
151
+ if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
152
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
186
153
  } catch (error) { return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
187
154
 
188
- if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
189
155
  const execution = validateLifecycleExecution(request.taskRef, {
190
156
  mode: request.orchestrated ? 'orchestrated' : 'standalone',
191
157
  identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
@@ -206,14 +172,6 @@ function preflightReviewSummaryUnlocked(
206
172
  });
207
173
  const qualificationError = qualification.ok ? null : qualification;
208
174
  if (!detail.ok || !structure.ok || qualificationError) {
209
- if (!recovery && !request.dryRun) {
210
- try {
211
- recovery = beginArtifactRecovery({
212
- taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
213
- requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
214
- }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
215
- } catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId); }
216
- }
217
175
  const diagnostics = [
218
176
  ...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
219
177
  ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
@@ -221,33 +179,18 @@ function preflightReviewSummaryUnlocked(
221
179
  ].join('; ');
222
180
  return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
223
181
  artifactSha256, semanticDigest,
224
- ...(recovery ? { recovery: recoveryInfo(recovery) } : {})
225
182
  });
226
183
  }
227
- if (!recovery && existingIntent?.state === 'passed') {
184
+ if (request.dryRun) {
228
185
  return {
229
186
  ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
230
187
  status: 'passed', changed: false, error: null
231
188
  };
232
189
  }
233
- if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
234
- return {
235
- ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
236
- status: 'passed', changed: false, error: null
237
- };
238
- }
239
- try {
240
- recovery ??= beginArtifactRecovery({
241
- taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
242
- requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
243
- }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
244
- const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
245
- prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
246
- return {
247
- ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
248
- status: 'passed', changed: false, error: null
249
- };
250
- } catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest }); }
190
+ return {
191
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
192
+ status: 'passed', changed: false, error: null
193
+ };
251
194
  }
252
195
 
253
196
  function preflightReviewSummary(
@@ -273,19 +216,9 @@ function preflightReviewSummary(
273
216
  type ReviewSummaryCandidatePreparation = Readonly<{
274
217
  result: ReviewFinalizationResult;
275
218
  content: string;
276
- recovery?: ArtifactRecoveryContext;
277
219
  lockAlreadyHeld?: boolean;
278
220
  }>;
279
221
 
280
- function recoveryInfo(recovery: ArtifactRecoveryContext): NonNullable<ReviewFinalizationResult['recovery']> {
281
- return {
282
- recoveryId: recovery.recoveryId,
283
- candidatePath: recovery.stagingPath,
284
- baselineSha256: recovery.baselineSha256,
285
- baselineSemanticDigest: recovery.baselineSemanticDigest
286
- };
287
- }
288
-
289
222
  /** Prepare domain validation, summary bytes and provenance before any publication. */
290
223
  function prepareReviewSummaryCandidate(
291
224
  request: ReviewFinalizationRequest,
@@ -314,36 +247,9 @@ function prepareReviewSummaryCandidate(
314
247
  let taskContent: string;
315
248
  try { taskContent = (options.fileSystem?.readFileSync ?? DEFAULT_FILE_SYSTEM.readFileSync)(resolved.taskMdPath); }
316
249
  catch (error) { return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error)); }
317
- let recovery: ArtifactRecoveryContext | undefined;
318
- if (request.recoveryId) {
319
- try {
320
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact);
321
- if (!intent || intent.recoveryOperationId !== request.recoveryId) return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
322
- if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
323
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
324
- artifactContent = fs.existsSync(recovery.stagingPath)
325
- ? fs.readFileSync(recovery.stagingPath, 'utf8')
326
- : fs.readFileSync(recovery.formalPath, 'utf8');
327
- } catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error)); }
328
- }
329
250
  const artifactSha256 = sha256Content(artifactContent);
330
251
  const semanticDigest = canonicalSemanticDigest(artifactContent);
331
252
  const digests = { artifactSha256, semanticDigest };
332
- let repairIntent;
333
- try { repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact); }
334
- catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
335
- if (repairIntent?.state === 'consumed') {
336
- if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
337
- return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
338
- }
339
- }
340
- if (repairIntent?.state === 'passed'
341
- && (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
342
- repairIntent = undefined;
343
- }
344
- if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
345
- return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
346
- }
347
253
  const execution = validateLifecycleExecution(request.taskRef, {
348
254
  mode: request.orchestrated ? 'orchestrated' : 'standalone',
349
255
  identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
@@ -364,57 +270,15 @@ function prepareReviewSummaryCandidate(
364
270
  const schema = getArtifactSchema(spec.family)!;
365
271
  const structure = inspectArtifactContract(artifactContent, schema);
366
272
  if (!structure.ok) {
367
- if (!recovery && !request.dryRun && options.startRecovery) {
368
- try {
369
- recovery = beginArtifactRecovery({
370
- taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
371
- requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
372
- }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
373
- } catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
374
- }
375
- return {
376
- ...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
377
- artifactSha256, semanticDigest: structure.semanticDigest,
378
- ...(recovery ? { recovery: {
379
- recoveryId: recovery.recoveryId,
380
- candidatePath: recovery.stagingPath,
381
- baselineSha256: recovery.baselineSha256,
382
- baselineSemanticDigest: recovery.baselineSemanticDigest
383
- } } : {})
384
- }),
385
- ...(recovery ? { recovery } : {})
386
- };
273
+ return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
274
+ artifactSha256, semanticDigest: structure.semanticDigest
275
+ });
387
276
  }
388
- const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts);
277
+ const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
278
+ refreshNumericCounts: true
279
+ });
389
280
  if (!transformed.ok) return reject(transformed.code, transformed.message);
390
281
  const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
391
- if (!recovery && repairIntent?.state === 'commit-started') {
392
- if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
393
- return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
394
- }
395
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
396
- }
397
- if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
398
- if (transformed.changed) return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
399
- return {
400
- content: transformed.content,
401
- result: {
402
- ...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
403
- status: 'no-op',
404
- changed: false,
405
- error: null
406
- },
407
- lockAlreadyHeld: options.lockAlreadyHeld
408
- };
409
- }
410
- if (!recovery && !request.dryRun && options.startRecovery) {
411
- try {
412
- recovery = beginArtifactRecovery({
413
- taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
414
- requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
415
- }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
416
- } catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests); }
417
- }
418
282
  return {
419
283
  content: transformed.content,
420
284
  result: {
@@ -424,7 +288,6 @@ function prepareReviewSummaryCandidate(
424
288
  operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
425
289
  error: null
426
290
  },
427
- ...(recovery ? { recovery } : {}),
428
291
  lockAlreadyHeld: options.lockAlreadyHeld
429
292
  };
430
293
  }
@@ -434,51 +297,18 @@ function commitReviewSummaryProvenance(
434
297
  repoRoot: string,
435
298
  options: Readonly<{ afterPublish?: () => void }> = {}
436
299
  ): ReviewFinalizationResult {
437
- if (prepared.result.status === 'failed' || !prepared.recovery) return prepared.result;
300
+ return prepared.result;
301
+ }
302
+
303
+ function replaceCurrentArtifactAtomically(file: string, content: string): void {
304
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
438
305
  try {
439
- const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
440
- if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
441
- let committed;
442
- if (intent.state === 'passed' || intent.state === 'consumed') {
443
- return {
444
- ...prepared.result,
445
- status: 'no-op',
446
- changed: false,
447
- artifactSha256: intent.finalArtifactSha256,
448
- semanticDigest: intent.finalSemanticDigest,
449
- error: null
450
- };
451
- } else if (intent.state === 'commit-started') {
452
- const reconciled = reconcileArtifactRecovery(prepared.recovery, {
453
- lockAlreadyHeld: prepared.lockAlreadyHeld,
454
- afterPublish: options.afterPublish
455
- });
456
- committed = reconciled.intent;
457
- } else if (intent.state === 'awaiting-preflight-recovery') {
458
- const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
459
- prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
460
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
461
- } else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
462
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
463
- }
464
- if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
465
- prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
466
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
467
- } else if (intent.state === 'full-finalizer-ready') {
468
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
306
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
307
+ fs.renameSync(temporary, file);
308
+ } finally {
309
+ try { fs.unlinkSync(temporary); } catch (error) {
310
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
469
311
  }
470
- if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
471
- return {
472
- ...prepared.result,
473
- artifactSha256: committed.finalArtifactSha256,
474
- semanticDigest: committed.finalSemanticDigest,
475
- error: null
476
- };
477
- } catch (error) {
478
- if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
479
- return { ...prepared.result, status: 'failed', error: {
480
- code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
481
- } };
482
312
  }
483
313
  }
484
314
 
@@ -500,14 +330,30 @@ function finalizeReviewSummaryUnlocked(
500
330
  catch (error) { return failed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
501
331
  const prepared = prepareReviewSummaryCandidate(request, content, options);
502
332
  if (request.dryRun) return prepared.result;
503
- return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
333
+ if (prepared.result.status === 'failed' || !prepared.result.changed) return prepared.result;
334
+ try {
335
+ replaceCurrentArtifactAtomically(validated.artifact.path, prepared.content);
336
+ return {
337
+ ...prepared.result,
338
+ status: 'applied',
339
+ artifactSha256: sha256Content(prepared.content),
340
+ semanticDigest: canonicalSemanticDigest(prepared.content)
341
+ };
342
+ } catch (error) {
343
+ return failed(
344
+ request,
345
+ 'REVIEW_RECOVERY_COMMIT_FAILED',
346
+ `cannot update current review artifact: ${error instanceof Error ? error.message : String(error)}`,
347
+ resolved.taskId
348
+ );
349
+ }
504
350
  }
505
351
 
506
352
  function finalizeReviewSummary(
507
353
  request: ReviewFinalizationRequest,
508
354
  options: ReviewFinalizationOptions = {}
509
355
  ): ReviewFinalizationResult {
510
- const effectiveOptions = { ...options, startRecovery: !request.dryRun };
356
+ const effectiveOptions = options;
511
357
  if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
512
358
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
513
359
  if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
@@ -44,6 +44,7 @@ import { validateQualificationAudit } from "./qualification-audit.ts";
44
44
  import { getArtifactSchema } from "./artifact-schema.ts";
45
45
  import { inspectArtifactContract } from "./artifact-operations.ts";
46
46
  import type { VerificationShared } from "./verification-types.ts";
47
+ import { normalizeVerificationRecord } from './gate-policy.ts';
47
48
  import { manualValidationFinalSummaryProjectionMatches } from "./manual-validation-receipt.ts";
48
49
  import { readManualValidationCompletion } from "./manual-validation-completion.ts";
49
50
  import { sha256File } from "./artifact-receipts.ts";
@@ -1392,11 +1393,11 @@ function interpolate(template: any, taskDir: any, artifactFile: any): any {
1392
1393
  }
1393
1394
 
1394
1395
  function summarizeGate(checks: any): any {
1395
- if (checks.some((check: any) => check.status === "blocked")) {
1396
+ if (checks.some((check: any) => check.effectiveStatus === "blocked")) {
1396
1397
  return "blocked";
1397
1398
  }
1398
1399
 
1399
- if (checks.some((check: any) => check.status === "fail")) {
1400
+ if (checks.some((check: any) => check.effectiveStatus === "fail")) {
1400
1401
  return "fail";
1401
1402
  }
1402
1403
 
@@ -1405,9 +1406,9 @@ function summarizeGate(checks: any): any {
1405
1406
 
1406
1407
  function summarizeChecks(checks: any): any {
1407
1408
  const counts = {
1408
- pass: checks.filter((check: any) => check.status === "pass").length,
1409
- fail: checks.filter((check: any) => check.status === "fail").length,
1410
- blocked: checks.filter((check: any) => check.status === "blocked").length
1409
+ pass: checks.filter((check: any) => check.effectiveStatus === "pass").length,
1410
+ fail: checks.filter((check: any) => check.effectiveStatus === "fail").length,
1411
+ blocked: checks.filter((check: any) => check.effectiveStatus === "blocked").length
1411
1412
  };
1412
1413
 
1413
1414
  if (counts.blocked > 0) {
@@ -1422,7 +1423,7 @@ function buildAction(gate: any, checks: any): any {
1422
1423
  return "All declared checks passed";
1423
1424
  }
1424
1425
 
1425
- const firstFailure = checks.find((check: any) => check.status !== "pass");
1426
+ const firstFailure = checks.find((check: any) => check.effectiveStatus !== "pass");
1426
1427
  if (!firstFailure) {
1427
1428
  return "Review validation output";
1428
1429
  }
@@ -1484,16 +1485,16 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
1484
1485
  const checks = [];
1485
1486
  for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
1486
1487
  if (checkConfig === null) continue;
1487
- const result = await runCheck(type, {
1488
+ const result = normalizeVerificationRecord(await runCheck(type, {
1488
1489
  mode,
1489
1490
  skillName,
1490
1491
  taskDir: path.resolve(taskDir),
1491
1492
  artifactFile,
1492
1493
  repositoryRoot: repoRoot,
1493
1494
  config: checkConfig
1494
- }, shared);
1495
- checks.push(result);
1496
- if (result.status === "blocked") break;
1495
+ }, shared));
1496
+ checks.push(...(result.subchecks?.map(normalizeVerificationRecord) ?? [result]));
1497
+ if (checks.at(-1)?.effectiveStatus === "blocked") break;
1497
1498
  }
1498
1499
  const gate = summarizeGate(checks);
1499
1500
  return { gate, skill: skillName, checks, summary: summarizeChecks(checks), action: buildAction(gate, checks) };
@@ -1502,15 +1503,24 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
1502
1503
  const config = (verifyConfig.checks || {})[type];
1503
1504
  if (config === undefined) return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
1504
1505
  if (config === null) return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
1505
- return {
1506
- skill: skillName,
1507
- ...await runCheck(type, {
1506
+ const result = normalizeVerificationRecord(await runCheck(type, {
1508
1507
  skillName,
1509
1508
  taskDir: path.resolve(taskDir),
1510
1509
  artifactFile,
1511
1510
  repositoryRoot: repoRoot,
1512
1511
  config
1513
- }, shared)
1512
+ }, shared));
1513
+ if (!result.subchecks?.length) return { skill: skillName, ...result };
1514
+ const checks = result.subchecks.map(normalizeVerificationRecord);
1515
+ const gate = summarizeGate(checks);
1516
+ return {
1517
+ skill: skillName,
1518
+ ...result,
1519
+ status: gate,
1520
+ effectiveStatus: gate,
1521
+ reason: gate === 'pass' ? 'OK' : checks.find((check: any) => check.effectiveStatus !== 'pass')?.reason ?? 'CHECK_FAILED',
1522
+ action: buildAction(gate, checks),
1523
+ checks
1514
1524
  };
1515
1525
  }
1516
1526
 
@@ -20,6 +20,13 @@ type VerificationCheckResult = {
20
20
  message: string;
21
21
  fail_type?: string;
22
22
  warnings?: string[];
23
+ checkId?: string;
24
+ classification?: 'hard' | 'soft' | 'info';
25
+ effectiveStatus?: VerificationStatus;
26
+ reason?: string;
27
+ action?: string;
28
+ dependsOn?: string;
29
+ subchecks?: VerificationCheckResult[];
23
30
  };
24
31
  type VerificationContext = {
25
32
  skillName: string;
@@ -126,7 +126,7 @@ async function verifyTaskEvent(request: { taskRef: string; event: string; artifa
126
126
  } else {
127
127
  for (const check of spec.checks ?? []) {
128
128
  const payload = await engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
129
- const status = payload.status as 'pass' | 'fail' | 'blocked';
129
+ const status = (payload.effectiveStatus ?? payload.status) as 'pass' | 'fail' | 'blocked';
130
130
  if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
131
131
  invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
132
132
  break;
@@ -150,18 +150,34 @@ function statusLabel(status: unknown): string {
150
150
  function renderPayload(payload: Record<string, unknown>): string {
151
151
  const lines: string[] = [];
152
152
  if (Array.isArray(payload.checks)) {
153
- lines.push(`Verification: ${payload.gate} | Skill: ${payload.skill}`, '');
153
+ const gateMode = typeof payload.gate === 'string';
154
+ lines.push(gateMode
155
+ ? `Verification: ${payload.gate} | Skill: ${payload.skill}`
156
+ : `Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
154
157
  for (const check of payload.checks as Array<Record<string, unknown>>) {
155
- lines.push(` [${statusLabel(check.status)}] ${check.type} - ${check.message}`);
158
+ const raw = statusLabel(check.status);
159
+ const effective = statusLabel(check.effectiveStatus ?? check.status);
160
+ const reason = check.reason ?? 'CHECK_FAILED';
161
+ const message = check.message ?? reason;
162
+ lines.push(` [${effective}] ${check.checkId ?? check.type} (${check.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${message}; ${check.action ?? 'Review validation output'}`);
156
163
  }
157
- lines.push('', `Result: ${payload.summary} - ${payload.action}`);
164
+ const summary = payload.summary ?? (() => {
165
+ const checks = payload.checks as Array<Record<string, unknown>>;
166
+ const statuses = checks.map((check) => check.effectiveStatus ?? check.status);
167
+ 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` : ''}`;
168
+ })();
169
+ lines.push('', `Result: ${summary} - ${payload.action}`);
158
170
  } else {
159
171
  lines.push(`Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
160
- lines.push(` [${statusLabel(payload.status)}] ${payload.type} - ${payload.message}`);
161
- const summary = payload.status === 'pass' ? '1 passed, 0 failed'
162
- : payload.status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
163
- const action = payload.status === 'pass' ? 'Requested check passed'
164
- : payload.status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
172
+ const raw = statusLabel(payload.status);
173
+ const effective = statusLabel(payload.effectiveStatus ?? payload.status);
174
+ const reason = payload.reason ?? 'CHECK_FAILED';
175
+ lines.push(` [${effective}] ${payload.checkId ?? payload.type} (${payload.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${payload.message ?? reason}; ${payload.action ?? 'Review validation output'}`);
176
+ const status = payload.effectiveStatus ?? payload.status;
177
+ const summary = status === 'pass' ? '1 passed, 0 failed'
178
+ : status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
179
+ const action = status === 'pass' ? 'Requested check passed'
180
+ : status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
165
181
  : `Fix ${payload.type} issues and re-run check`;
166
182
  lines.push('', `Result: ${summary} - ${action}`);
167
183
  }
@@ -9,10 +9,13 @@ export const TASK_WORKFLOW_COMMANDS = Object.freeze({
9
9
  'ledger-finding-response': ['task-ledger', 'finding-respond'],
10
10
  'ledger-finding-review': ['task-ledger', 'finding-review'],
11
11
  'ledger-finding-upsert': ['task-ledger', 'finding-upsert'],
12
+ 'ledger-stage-status': ['task-ledger', 'stage-status'],
12
13
  'decision-next-id': ['task-ledger', 'decision-next-id'],
13
14
  'decision-upsert': ['task-ledger', 'decision-upsert'],
14
15
  'invalidation-reconcile': ['task-invalidation', 'reconcile'],
15
- 'warning-add': ['task-warning', 'add']
16
+ 'warning-add': ['task-warning', 'add'],
17
+ 'warning-list': ['task-warning', 'list'],
18
+ 'warning-set-status': ['task-warning', 'set-status']
16
19
  } as const);
17
20
  export type TaskWorkflowOperation = keyof typeof TASK_WORKFLOW_COMMANDS;
18
21
  export const TASK_WORKFLOW_OPERATIONS = Object.freeze(Object.keys(TASK_WORKFLOW_COMMANDS) as TaskWorkflowOperation[]);
@@ -1,6 +1,10 @@
1
+ import fs from 'node:fs';
2
+
1
3
  import { reconcileTaskInvalidation } from './invalidation-command.ts';
2
4
  import { applyLedgerIntent } from './ledger-intents.ts';
3
5
  import type { LedgerIntent } from './ledger-intents.ts';
6
+ import { isReviewStage, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
7
+ import { resolveTaskRef } from './resolve-ref.ts';
4
8
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
5
9
  import type { TaskWorkflowOperation } from './workflow-command.ts';
6
10
  import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
@@ -116,7 +120,24 @@ function ledgerIntent(operation: TaskWorkflowOperation, args: readonly string[])
116
120
 
117
121
  function warningIntent(args: readonly string[]): WorkflowWarningIntent | DispatchFailure {
118
122
  const [taskRef, kind] = args;
119
- if (!taskRef || kind !== 'add') return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
123
+ if (!taskRef || !['add', 'list', 'set-status'].includes(kind ?? '')) {
124
+ return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
125
+ }
126
+ if (kind === 'list') {
127
+ const values = options(args, { '--status': 'status' });
128
+ if (isFailure(values)) return values;
129
+ return { kind, taskRef, ...(value(values, '--status') ? { status: value(values, '--status') as never } : {}) };
130
+ }
131
+ if (kind === 'set-status') {
132
+ const values = options(args, { '--id': 'id', '--status': 'status', '--resolution': 'resolution', '--dry-run': 'dryRun' });
133
+ if (isFailure(values)) return values;
134
+ const missing = required(values, ['--id', '--status', '--resolution']);
135
+ if (missing) return missing;
136
+ return {
137
+ kind, taskRef, id: value(values, '--id')!, status: value(values, '--status') as never,
138
+ resolution: value(values, '--resolution')!, ...(values['--dry-run'] === true ? { dryRun: true } : {})
139
+ };
140
+ }
120
141
  const values = options(args, {
121
142
  '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
122
143
  '--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
@@ -131,6 +152,26 @@ function warningIntent(args: readonly string[]): WorkflowWarningIntent | Dispatc
131
152
  } as WorkflowWarningIntent;
132
153
  }
133
154
 
155
+ function stageStatus(repoRoot: string, args: readonly string[]): WorkflowDispatchResult {
156
+ const [taskRef, kind] = args;
157
+ if (!taskRef || kind !== 'stage-status') return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
158
+ const values = options(args, { '--stage': 'stage' });
159
+ if (isFailure(values)) return values;
160
+ const stage = value(values, '--stage');
161
+ if (!stage || !isReviewStage(stage)) return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'stage-status requires a valid review stage');
162
+ const resolved = resolveTaskRef(taskRef, { repoRoot });
163
+ if (!resolved.ok) return failed(resolved.code, resolved.message);
164
+ try {
165
+ const ledger = parseLedgerDocument(fs.readFileSync(resolved.taskMdPath, 'utf8'));
166
+ if (!ledger.present) return failed('LEDGER_SECTION_MISSING', 'review disagreement ledger section is missing');
167
+ const invalid = validateLedgerRows(ledger.rows);
168
+ if (invalid) return failed(invalid.code, invalid.message);
169
+ return { status: 'ready', changed: false, taskId: resolved.taskId, stageStatus: summarizeLedgerStage(ledger.rows, stage), error: null };
170
+ } catch (error) {
171
+ return failed('LEDGER_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error));
172
+ }
173
+ }
174
+
134
175
  /**
135
176
  * The caller has already selected and authorized the execution boundary.
136
177
  * This function deliberately invokes task domains in that process: starting a
@@ -157,10 +198,12 @@ export function dispatchWorkflowCommand(
157
198
  ...(values['--dry-run'] === true ? { dryRun: true } : {})
158
199
  }));
159
200
  }
160
- if (operation === 'warning-add') {
201
+ if (operation === 'ledger-stage-status') return stageStatus(repoRoot, args);
202
+ if (operation === 'warning-add' || operation === 'warning-list' || operation === 'warning-set-status') {
161
203
  const intent = warningIntent(args);
162
204
  if (isFailure(intent)) return intent;
163
- return withTaskExecutionLock(repoRoot, taskRef, 'task-warning.add', () => applyWorkflowWarningIntent(intent, { repoRoot }));
205
+ if (intent.kind === 'list') return applyWorkflowWarningIntent(intent, { repoRoot });
206
+ return withTaskExecutionLock(repoRoot, taskRef, `task-warning.${intent.kind}`, () => applyWorkflowWarningIntent(intent, { repoRoot }));
164
207
  }
165
208
  const intent = ledgerIntent(operation, args);
166
209
  if (isFailure(intent)) return intent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,7 +52,7 @@
52
52
  "installer"
53
53
  ],
54
54
  "dependencies": {
55
- "@clack/prompts": "1.7.0",
55
+ "@clack/prompts": "1.8.1",
56
56
  "@larksuiteoapi/node-sdk": "^1.68.0",
57
57
  "cross-spawn": "^7.0.6",
58
58
  "fs-native-extensions": "1.5.1",