@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
@@ -1,13 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
3
4
  import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
4
- import { buildArtifactLinkSection, inspectArtifactDirectory, parseReviewedInputReference, parseCodePlanInputReference, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
5
+ import { buildArtifactLinkSection, inspectArtifactDirectory, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
5
6
  import { artifactName, parseArtifactName } from './artifact-name.js';
6
7
  import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
7
8
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
8
9
  import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
9
10
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
10
11
  import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.js';
12
+ import { extractReviewBaseline, extractReviewedHead, extractReviewedSnapshotTree } from './review-fingerprint.js';
11
13
  import { resolveTaskRef } from './resolve-ref.js';
12
14
  import { findSectionHeading } from './sections.js';
13
15
  import { validateLifecycleExecution } from './lifecycle-execution.js';
@@ -20,6 +22,7 @@ import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artif
20
22
  import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
21
23
  import { buildLifecycleFacts, canStart } from './capabilities.js';
22
24
  import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
25
+ import { reconcileTaskInvalidation } from './invalidation-command.js';
23
26
  import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
24
27
  import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.js';
25
28
  import { getArtifactSchema } from './artifact-schema.js';
@@ -36,6 +39,27 @@ const eventCatalog = [
36
39
  'manual-validation.started', 'manual-validation.completed',
37
40
  'validation-run.started', 'validation-run.completed'
38
41
  ];
42
+ function approvedCleanReviewedCommit(reviewContent, verdict, repoRoot) {
43
+ if (verdict !== 'approved')
44
+ return null;
45
+ const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
46
+ const reviewedTree = extractReviewedSnapshotTree(reviewContent);
47
+ if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree))
48
+ return null;
49
+ try {
50
+ const currentHead = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
51
+ if (currentHead !== reviewedHead)
52
+ return null;
53
+ const currentTree = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
54
+ if (currentTree !== reviewedTree)
55
+ return null;
56
+ execFileSync('git', ['-C', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
57
+ return reviewedHead;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
39
63
  const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
40
64
  const SCHEMAS = {
41
65
  'analyze.started': { optional: ['round'] },
@@ -334,7 +358,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
334
358
  }
335
359
  intent = reconciled.intent;
336
360
  }
337
- if (!['passed', 'consumed'].includes(intent.state)) {
361
+ if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
338
362
  return {
339
363
  code: 'EVENT_ARTIFACT_CONFLICT',
340
364
  message: `review finalizer provenance is incomplete for ${artifact}`
@@ -345,9 +369,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
345
369
  return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
346
370
  }
347
371
  const content = fs.readFileSync(validated.artifact.path, 'utf8');
348
- const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
349
372
  if (intent.round !== round
350
- || intent.requestId !== expectedRequestId
351
373
  || intent.finalArtifactSha256 !== sha256Content(content)
352
374
  || intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
353
375
  return {
@@ -355,7 +377,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
355
377
  message: `review finalizer provenance does not match ${artifact}`
356
378
  };
357
379
  }
358
- if (intent.state === 'passed') {
380
+ if (intent.state === 'passed' || intent.state === 'consumption-started') {
359
381
  const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
360
382
  writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
361
383
  }
@@ -595,34 +617,22 @@ function reworkIntentMutationForCompletion(content, taskDir, family, artifact, t
595
617
  }
596
618
  function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
597
619
  if (family === 'code') {
598
- let content;
599
- try {
600
- content = fs.readFileSync(artifact.path, 'utf8');
601
- }
602
- catch (error) {
603
- return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
604
- }
605
- const input = parseCodePlanInputReference(content);
606
- if (!input)
607
- return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
608
620
  const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
609
621
  const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
610
622
  if (!startedInput || !startedSha256)
611
623
  return { ok: false, message: 'code.started plan input context is missing' };
612
- if (input.name !== startedInput)
613
- return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
614
624
  const plan = inspectArtifactDirectory(taskDir, 'plan');
615
- if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
616
- return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
625
+ if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
626
+ return { ok: false, message: `code input '${startedInput}' is not the latest plan artifact` };
617
627
  }
618
628
  try {
619
629
  const inputSha256 = sha256File(plan.latest.path);
620
630
  if (inputSha256 !== startedSha256)
621
- return { ok: false, message: `code input ${input.name} changed after code.started` };
631
+ return { ok: false, message: `code input ${startedInput} changed after code.started` };
622
632
  return {
623
633
  ok: true,
624
634
  receipt: {
625
- event: 'code.completed', output: artifact.name, input: input.name,
635
+ event: 'code.completed', output: artifact.name, input: startedInput,
626
636
  inputSha256, completedAt
627
637
  }
628
638
  };
@@ -634,25 +644,13 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
634
644
  if (!family.startsWith('review-'))
635
645
  return null;
636
646
  const expectedFamily = reviewInputFamily(family);
637
- let content;
638
- try {
639
- content = fs.readFileSync(artifact.path, 'utf8');
640
- }
641
- catch (error) {
642
- return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
643
- }
644
- const input = parseReviewedInputReference(content, expectedFamily);
645
- if (!input)
646
- return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
647
647
  const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
648
648
  const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
649
649
  if (!startedInput || !startedSha256)
650
650
  return { ok: false, message: 'review started input context is missing' };
651
- if (input.name !== startedInput)
652
- return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
653
651
  const current = inspectArtifactDirectory(taskDir, expectedFamily);
654
- if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
655
- return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
652
+ if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
653
+ return { ok: false, message: `review input '${startedInput}' is not the latest ${expectedFamily} artifact` };
656
654
  }
657
655
  const event = family === 'review-analysis'
658
656
  ? 'review-analysis.completed'
@@ -663,17 +661,17 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
663
661
  throw new Error('review input is not a regular file');
664
662
  const inputSha256 = sha256File(current.latest.path);
665
663
  if (inputSha256 !== startedSha256)
666
- return { ok: false, message: `review input ${input.name} changed after review.started` };
664
+ return { ok: false, message: `review input ${startedInput} changed after review.started` };
667
665
  return {
668
666
  ok: true,
669
667
  receipt: {
670
- event, output: artifact.name, input: input.name,
668
+ event, output: artifact.name, input: startedInput,
671
669
  inputSha256, completedAt
672
670
  }
673
671
  };
674
672
  }
675
673
  catch (error) {
676
- return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
674
+ return { ok: false, message: `cannot hash review input ${startedInput}: ${error instanceof Error ? error.message : String(error)}` };
677
675
  }
678
676
  }
679
677
  function applyTaskEventUnlocked(request, options = {}) {
@@ -693,6 +691,24 @@ function applyTaskEventUnlocked(request, options = {}) {
693
691
  catch (error) {
694
692
  return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
695
693
  }
694
+ const initialParts = eventParts(request.event);
695
+ if (initialParts.phase === 'started' && !request.dryRun) {
696
+ const reconciled = reconcileTaskInvalidation(request.taskRef, { repoRoot: resolved.repoRoot });
697
+ if (reconciled.status === 'failed') {
698
+ return failed(request, {
699
+ code: 'EVENT_TRANSITION_INVALID',
700
+ message: `INVALIDATION_RECONCILE_FAILED: ${reconciled.error?.code ?? 'UNKNOWN'}: ${reconciled.error?.message ?? ''}`
701
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
702
+ }
703
+ if (reconciled.changed) {
704
+ try {
705
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
706
+ }
707
+ catch (error) {
708
+ return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
709
+ }
710
+ }
711
+ }
696
712
  let frontmatter;
697
713
  try {
698
714
  frontmatter = parseTypedTaskFrontmatter(content);
@@ -706,7 +722,6 @@ function applyTaskEventUnlocked(request, options = {}) {
706
722
  const rows = startedBackedRows(pairEntries(section.entries));
707
723
  let normalized = request;
708
724
  let artifactContext = null;
709
- const initialParts = eventParts(request.event);
710
725
  if (initialParts.phase === 'started') {
711
726
  const openIdentity = openStartedIdentity(rows, initialParts.family);
712
727
  if (openIdentity && 'conflict' in openIdentity)
@@ -935,20 +950,15 @@ function applyTaskEventUnlocked(request, options = {}) {
935
950
  if (reconciled.status === 'passed' || reconciled.status === 'consumed')
936
951
  reviewFinalizationIntent = reconciled.intent;
937
952
  }
938
- if (!reviewFinalizationIntent || !['passed', 'consumed'].includes(reviewFinalizationIntent.state)) {
953
+ if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
939
954
  return failed(normalized, {
940
955
  code: 'EVENT_ARTIFACT_CONFLICT',
941
956
  message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
942
957
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
943
958
  }
944
- const reviewStage = eventIdentity.family === 'review-analysis'
945
- ? 'analysis'
946
- : eventIdentity.family === 'review-plan' ? 'plan' : 'code';
947
959
  const actualSha256 = sha256File(completedArtifact.path);
948
960
  const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
949
- const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
950
961
  if (reviewFinalizationIntent.round !== normalized.round
951
- || reviewFinalizationIntent.requestId !== expectedRequestId
952
962
  || reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
953
963
  || reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
954
964
  return failed(normalized, {
@@ -1060,6 +1070,13 @@ function applyTaskEventUnlocked(request, options = {}) {
1060
1070
  else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
1061
1071
  frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
1062
1072
  }
1073
+ if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
1074
+ const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot);
1075
+ if (reviewedCommit)
1076
+ frontmatterSet.last_reviewed_commit = reviewedCommit;
1077
+ else if (normalized.verdict === 'approved')
1078
+ frontmatterSet.last_reviewed_commit = '';
1079
+ }
1063
1080
  if (eventIdentity.phase === 'completed')
1064
1081
  frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
1065
1082
  if (eventIdentity.phase === 'started' && normalized.implementationInput)
@@ -1106,7 +1123,7 @@ function applyTaskEventUnlocked(request, options = {}) {
1106
1123
  const expectedIntent = localFinalizationIntent;
1107
1124
  localFinalizationIntent = {
1108
1125
  ...localFinalizationIntent,
1109
- state: 'commit-started',
1126
+ state: 'consumption-started',
1110
1127
  phase: 'task-event.completed',
1111
1128
  authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
1112
1129
  requestId: lifecycleAuthority.lifecycleRequestId,
@@ -1138,7 +1155,7 @@ function applyTaskEventUnlocked(request, options = {}) {
1138
1155
  const expectedIntent = reviewFinalizationIntent;
1139
1156
  reviewFinalizationIntent = {
1140
1157
  ...reviewFinalizationIntent,
1141
- state: 'commit-started',
1158
+ state: 'consumption-started',
1142
1159
  phase: 'task-event.completed',
1143
1160
  updatedAt: Date.now()
1144
1161
  };
@@ -1179,7 +1196,7 @@ function applyTaskEventUnlocked(request, options = {}) {
1179
1196
  }
1180
1197
  if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
1181
1198
  try {
1182
- if (reviewFinalizationIntent.state === 'commit-started') {
1199
+ if (reviewFinalizationIntent.state === 'consumption-started') {
1183
1200
  const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
1184
1201
  writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
1185
1202
  }
@@ -1230,7 +1247,7 @@ function applyTaskEvent(request, options = {}) {
1230
1247
  const parts = eventParts(request.event);
1231
1248
  if (options.lockAlreadyHeld)
1232
1249
  return applyTaskEventUnlocked(request, options);
1233
- if (request.dryRun || parts.phase !== 'completed' || parts.family === 'manual-validation') {
1250
+ if (request.dryRun || !['started', 'completed'].includes(parts.phase)) {
1234
1251
  return applyTaskEventUnlocked(request, options);
1235
1252
  }
1236
1253
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
@@ -0,0 +1,94 @@
1
+ const OPTIONAL_PLATFORM_AUDITS = new Set([
2
+ 'closed-status-labels', 'status-label', 'comment-marker', 'pr-comment-marker',
3
+ 'pr-comment-last-commit', 'pr-comment-content', 'comment-content', 'task-comment-content',
4
+ 'in-labels-computed', 'pr-type-label', 'in-labels-match-pr', 'pr-assignee',
5
+ 'requirements', 'issue-type', 'issue-fields', 'milestone'
6
+ ]);
7
+ const STATUS_LABELS = {
8
+ 'create-task': 'status: waiting-for-triage',
9
+ 'analyze-task': 'status: pending-design-work',
10
+ 'review-analysis': 'status: pending-design-work',
11
+ 'plan-task': 'status: pending-design-work',
12
+ 'review-plan': 'status: pending-design-work',
13
+ 'code-task': 'status: in-progress',
14
+ 'review-code': 'status: in-progress'
15
+ };
16
+ const ARTIFACT_COMMENT_SKILLS = new Set([
17
+ 'analyze-task', 'plan-task', 'review-analysis', 'review-plan', 'code-task',
18
+ 'review-code', 'review-pr', 'run-manual-validation'
19
+ ]);
20
+ const PR_AUDIT_SKILLS = new Set(['create-pr']);
21
+ const IN_LABEL_AUDIT_SKILLS = new Set(['code-task', 'commit', 'create-pr']);
22
+ export function platformAuditPolicy(checkId, context) {
23
+ const skillName = context.skillName || '';
24
+ const artifactComment = Boolean(context.artifactFile) && ARTIFACT_COMMENT_SKILLS.has(skillName);
25
+ const prAudit = PR_AUDIT_SKILLS.has(skillName);
26
+ const inLabelAudit = IN_LABEL_AUDIT_SKILLS.has(skillName);
27
+ switch (checkId) {
28
+ case 'status-label':
29
+ return { classification: 'soft', enabled: Boolean(STATUS_LABELS[skillName]), expectedStatusLabel: STATUS_LABELS[skillName] };
30
+ case 'comment-marker':
31
+ return {
32
+ classification: 'soft',
33
+ enabled: artifactComment || skillName === 'complete-task',
34
+ ...(artifactComment ? { expectedCommentMarkerKey: 'artifact' } : skillName === 'complete-task' ? { expectedCommentMarkerKey: 'summary' } : {})
35
+ };
36
+ case 'pr-comment-marker':
37
+ return {
38
+ classification: 'soft',
39
+ enabled: skillName === 'commit' || prAudit,
40
+ ...(skillName === 'commit' || prAudit ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
41
+ };
42
+ case 'pr-comment-last-commit':
43
+ return {
44
+ classification: 'soft',
45
+ enabled: skillName === 'commit',
46
+ ...(skillName === 'commit' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
47
+ };
48
+ case 'pr-comment-content':
49
+ return {
50
+ classification: 'soft',
51
+ enabled: skillName === 'complete-manual-validation',
52
+ ...(skillName === 'complete-manual-validation' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
53
+ };
54
+ case 'comment-content':
55
+ return { classification: 'soft', enabled: artifactComment, expectedCommentMarkerKey: 'artifact' };
56
+ case 'task-comment-content':
57
+ return { classification: 'soft', enabled: true };
58
+ case 'in-labels-computed':
59
+ return { classification: 'soft', enabled: inLabelAudit };
60
+ case 'pr-type-label':
61
+ case 'in-labels-match-pr':
62
+ case 'pr-assignee':
63
+ return { classification: 'soft', enabled: prAudit };
64
+ case 'requirements':
65
+ return { classification: 'soft', enabled: skillName === 'complete-task' };
66
+ case 'milestone':
67
+ return { classification: 'soft', enabled: true, requireSpecificMilestone: skillName === 'code-task' };
68
+ case 'closed-status-labels':
69
+ case 'issue-type':
70
+ case 'issue-fields':
71
+ return { classification: 'soft', enabled: true };
72
+ }
73
+ }
74
+ export function classificationForCheck(checkId) {
75
+ return checkId.startsWith('platform.') && OPTIONAL_PLATFORM_AUDITS.has(checkId.slice('platform.'.length))
76
+ ? 'soft'
77
+ : 'hard';
78
+ }
79
+ export function effectiveStatus(status, classification) {
80
+ return classification === 'hard' ? status : 'pass';
81
+ }
82
+ export function normalizeVerificationRecord(record) {
83
+ const checkId = record.checkId ?? record.type;
84
+ const classification = record.classification ?? classificationForCheck(checkId);
85
+ return {
86
+ ...record,
87
+ checkId,
88
+ classification,
89
+ effectiveStatus: record.effectiveStatus ?? effectiveStatus(record.status, classification),
90
+ reason: record.reason ?? record.fail_type ?? (classification === 'info' ? 'NOT_APPLICABLE' : record.status === 'pass' ? 'OK' : 'CHECK_FAILED'),
91
+ action: record.action ?? (classification === 'info' || record.status === 'pass' ? 'No action required' : 'Review the check evidence and retry after correction')
92
+ };
93
+ }
94
+ //# sourceMappingURL=gate-policy.js.map
@@ -292,12 +292,37 @@ function cleanupConsumedRecoveryFiles(repoRoot, task, candidate) {
292
292
  return false;
293
293
  }
294
294
  for (const entry of entries) {
295
- if (entry.isSymbolicLink() || !entry.isFile() || !['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name))
295
+ if (entry.isSymbolicLink())
296
+ return false;
297
+ if (entry.isFile()) {
298
+ if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name))
299
+ return false;
300
+ continue;
301
+ }
302
+ if (!entry.isDirectory() || entry.name !== 'generations')
303
+ return false;
304
+ let generations;
305
+ try {
306
+ generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true });
307
+ }
308
+ catch {
309
+ return false;
310
+ }
311
+ if (generations.some((generation) => generation.isSymbolicLink()
312
+ || !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name)))
296
313
  return false;
297
314
  }
298
315
  try {
299
- for (const entry of entries)
300
- fs.unlinkSync(path.join(recovery, entry.name));
316
+ for (const entry of entries) {
317
+ const target = path.join(recovery, entry.name);
318
+ if (entry.isFile())
319
+ fs.unlinkSync(target);
320
+ else {
321
+ for (const generation of fs.readdirSync(target, { withFileTypes: true }))
322
+ fs.unlinkSync(path.join(target, generation.name));
323
+ fs.rmdirSync(target);
324
+ }
325
+ }
301
326
  fs.rmdirSync(recovery);
302
327
  const recoveryRoot = path.dirname(recovery);
303
328
  if (fs.readdirSync(recoveryRoot).length === 0)
@@ -2,13 +2,13 @@ import fs from 'node:fs';
2
2
  import { inspectDecisionDetailDuplicates } from './decision-details.js';
3
3
  import { scanVisibleMarkdown } from './markdown.js';
4
4
  import { parseArtifactName } from './artifact-name.js';
5
- import { hasOpenArtifactRound, parseCodePlanInputReference, validateArtifactPublication, validateCompletedArtifact } from './artifact-lifecycle.js';
5
+ import { hasOpenArtifactRound, validateArtifactPublication, validateCompletedArtifact } from './artifact-lifecycle.js';
6
6
  import { resolveTaskRef } from './resolve-ref.js';
7
7
  import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
8
8
  import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.js';
9
9
  import { getArtifactSchema } from './artifact-schema.js';
10
10
  import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
11
- import { beginArtifactRecovery, commitArtifactRecovery, consumeArtifactRecovery, prepareArtifactRecoveryCommit, recordArtifactRecoveryPassed, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
11
+ import { beginArtifactRecovery, commitArtifactRecovery, consumeArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, recordArtifactRecoveryPassed, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
12
12
  import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
13
13
  const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
14
14
  analysis: getArtifactSchema('analysis').sections.map((section) => section.headings.zh),
@@ -97,13 +97,16 @@ function validateLocalArtifact(content, options) {
97
97
  if (!decisionDetails.ok)
98
98
  diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
99
99
  if (options.taskContent !== undefined) {
100
- const expected = expectedQualificationRelations(options.taskContent, options.family);
101
- if (!expected.ok)
100
+ const identity = options.artifact ? parseArtifactName(options.artifact) : null;
101
+ const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
102
+ ? expectedQualificationRelations(options.taskContent, options.family)
103
+ : undefined;
104
+ if (expected && !expected.ok)
102
105
  diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
103
106
  const qualification = validateQualificationAudit(options.taskContent, content, {
104
107
  family: options.family,
105
108
  artifact: options.artifact,
106
- expectedUpstreamRelations: expected.ok ? expected.relations : undefined
109
+ expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
107
110
  });
108
111
  if (!qualification.ok)
109
112
  diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
@@ -136,7 +139,7 @@ function tupleFor(request, taskId, authority) {
136
139
  authorityDigest: authority ? authorityDigest(authority) : null
137
140
  };
138
141
  }
139
- function prepareLocalArtifact(request, candidate, authority) {
142
+ function prepareLocalArtifact(request, candidate, authority, preflightOnly = false) {
140
143
  const failed = (code, message, extra = {}) => ({
141
144
  result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
142
145
  });
@@ -152,14 +155,16 @@ function prepareLocalArtifact(request, candidate, authority) {
152
155
  let taskContent;
153
156
  let content;
154
157
  let recovery;
158
+ let recoveryState;
155
159
  try {
156
160
  taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
157
161
  if (request.recoveryId) {
158
162
  const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
159
163
  if (!intent || intent.recoveryOperationId !== request.recoveryId)
160
164
  return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'recovery id does not match the artifact journal');
161
- if (!['awaiting-recovery', 'finalize-ready', 'commit-started'].includes(intent.state))
165
+ if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
162
166
  return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
167
+ recoveryState = intent.state;
163
168
  recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
164
169
  content = fs.existsSync(recovery.stagingPath)
165
170
  ? fs.readFileSync(recovery.stagingPath, 'utf8')
@@ -192,24 +197,63 @@ function prepareLocalArtifact(request, candidate, authority) {
192
197
  if (request.recoveryId) {
193
198
  if (!validation.ok)
194
199
  return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
200
+ if (preflightOnly && recovery && recoveryState === 'awaiting-preflight-recovery') {
201
+ const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
202
+ prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
203
+ }
195
204
  return {
196
- result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(recovery) },
205
+ result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(recovery) },
197
206
  content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
198
207
  };
199
208
  }
200
209
  const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
201
210
  if (existing?.state === 'passed' || existing?.state === 'consumed') {
202
- if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
211
+ if (existing.state === 'consumed' && (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest)) {
203
212
  return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
204
213
  }
214
+ if (existing.finalArtifactSha256 === artifactSha256 && existing.finalSemanticDigest === validation.semanticDigest) {
215
+ const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
216
+ return {
217
+ result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
218
+ content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
219
+ };
220
+ }
221
+ }
222
+ if (existing && ['preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready'].includes(existing.state)) {
223
+ const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
224
+ return {
225
+ result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
226
+ content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
227
+ };
228
+ }
229
+ if (existing?.state === 'commit-started') {
230
+ if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
231
+ return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match the interrupted recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
232
+ }
205
233
  const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
234
+ const reconciled = reconcileArtifactRecovery(context, {
235
+ lockAlreadyHeld: request.lockAlreadyHeld,
236
+ validateFinal: () => validation.semanticDigest === existing.finalSemanticDigest
237
+ });
238
+ if (reconciled.status !== 'passed') {
239
+ return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `interrupted recovery could not be reconciled: ${reconciled.status}`), content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
240
+ }
206
241
  return {
207
- result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
242
+ result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
208
243
  content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
209
244
  };
210
245
  }
211
246
  try {
212
247
  if (validation.ok) {
248
+ if (preflightOnly) {
249
+ const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
250
+ repoRoot: resolved.repoRoot, taskDir: resolved.taskDir,
251
+ ...(authority ? { recoveryId: authority.operationId } : {}), lockAlreadyHeld: request.lockAlreadyHeld
252
+ });
253
+ const staged = stageArtifactCandidate(context, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
254
+ prepareArtifactRecoveryCommit(context, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
255
+ return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
256
+ }
213
257
  const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
214
258
  repoRoot: resolved.repoRoot,
215
259
  taskDir: resolved.taskDir,
@@ -219,7 +263,7 @@ function prepareLocalArtifact(request, candidate, authority) {
219
263
  lockAlreadyHeld: request.lockAlreadyHeld
220
264
  });
221
265
  recovery = context;
222
- return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
266
+ return { result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
223
267
  }
224
268
  const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
225
269
  repoRoot: resolved.repoRoot,
@@ -237,7 +281,7 @@ function prepareLocalArtifact(request, candidate, authority) {
237
281
  return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', String(error)), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
238
282
  }
239
283
  }
240
- function commitLocalArtifactProvenance(prepared) {
284
+ function commitLocalArtifactProvenance(prepared, options = {}) {
241
285
  if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery)
242
286
  return prepared.result;
243
287
  if (prepared.result.status === 'failed')
@@ -250,30 +294,48 @@ function commitLocalArtifactProvenance(prepared) {
250
294
  if (intent.state === 'passed' || intent.state === 'consumed') {
251
295
  return {
252
296
  ...prepared.result,
297
+ changed: false,
253
298
  artifactSha256: intent.finalArtifactSha256,
254
299
  semanticDigest: intent.finalSemanticDigest,
255
300
  error: null
256
301
  };
257
302
  }
258
- else if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
259
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
303
+ else if (intent.state === 'commit-started') {
304
+ const reconciled = reconcileArtifactRecovery(prepared.recovery, {
305
+ lockAlreadyHeld: prepared.lockAlreadyHeld,
306
+ afterPublish: options.afterPublish
307
+ });
308
+ committed = reconciled.intent;
260
309
  }
261
- else {
310
+ else if (intent.state === 'awaiting-preflight-recovery') {
262
311
  const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
263
312
  prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
264
313
  committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
265
314
  }
266
- if (committed.state !== 'passed')
267
- throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
315
+ if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
316
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
317
+ }
318
+ if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
319
+ prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
320
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
321
+ }
322
+ else if (intent.state === 'full-finalizer-ready') {
323
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
324
+ }
325
+ if (!committed || committed.state !== 'passed')
326
+ throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
268
327
  return {
269
328
  ...prepared.result,
270
329
  status: 'passed',
330
+ changed: false,
271
331
  artifactSha256: committed.finalArtifactSha256,
272
332
  semanticDigest: committed.finalSemanticDigest,
273
333
  error: null
274
334
  };
275
335
  }
276
336
  catch (error) {
337
+ if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
338
+ throw error;
277
339
  return { ...prepared.result, status: 'failed', error: {
278
340
  code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
279
341
  } };
@@ -295,5 +357,10 @@ function finalizeLocalArtifact(request, authority, options = {}) {
295
357
  }
296
358
  return result;
297
359
  }
298
- export { LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, prepareLocalArtifact, commitLocalArtifactProvenance, readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent, canonicalSemanticDigest as semanticDigest, sha256Content, validateLocalArtifact };
360
+ /** Validate and seal an immutable preflight generation without publishing the formal artifact. */
361
+ function preflightLocalArtifact(request, authority) {
362
+ const prepared = prepareLocalArtifact(request, undefined, authority, true);
363
+ return prepared.result;
364
+ }
365
+ export { LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, preflightLocalArtifact, prepareLocalArtifact, commitLocalArtifactProvenance, readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent, canonicalSemanticDigest as semanticDigest, sha256Content, validateLocalArtifact };
299
366
  //# sourceMappingURL=local-artifact-finalization.js.map