@fitlab-ai/agent-infra 0.10.0 → 0.11.0

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 (95) 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/platform-comment.js +60 -2
  7. package/dist/lib/internal/task-artifact.js +1 -1
  8. package/dist/lib/internal/task-finalization.js +2 -2
  9. package/dist/lib/internal/task-lifecycle.js +2 -2
  10. package/dist/lib/internal/task-operation-registry.js +22 -12
  11. package/dist/lib/internal/task-orchestration.js +2 -2
  12. package/dist/lib/internal/task-review.js +10 -5
  13. package/dist/lib/platform/activity-recovery.js +159 -0
  14. package/dist/lib/platform/issue-comments.js +84 -32
  15. package/dist/lib/platform/task-comment-projection.js +70 -0
  16. package/dist/lib/platform/task-recovery.js +55 -0
  17. package/dist/lib/platform/verification-sync.js +43 -9
  18. package/dist/lib/sandbox/control/executor.js +4 -2
  19. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  20. package/dist/lib/sandbox/control/workflow-executor.js +83 -21
  21. package/dist/lib/server/process-state.js +17 -5
  22. package/dist/lib/task/artifact-command.js +6 -3
  23. package/dist/lib/task/artifact-operations.js +1 -1
  24. package/dist/lib/task/artifact-recovery.js +114 -68
  25. package/dist/lib/task/artifact-repair-intent.js +25 -8
  26. package/dist/lib/task/control-authority.js +1 -1
  27. package/dist/lib/task/events.js +6 -6
  28. package/dist/lib/task/intermediate-cleanup.js +28 -3
  29. package/dist/lib/task/local-artifact-finalization.js +78 -14
  30. package/dist/lib/task/review-command.js +2 -2
  31. package/dist/lib/task/review-finalization.js +203 -10
  32. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  33. package/dist/lib/task/workflow-command.js +2 -0
  34. package/dist/lib/task/workflow-dispatch.js +161 -0
  35. package/dist/lifecycle-build-manifest.json +3 -3
  36. package/lib/internal/cli-route-inventory.ts +3 -4
  37. package/lib/internal/platform-comment.ts +53 -2
  38. package/lib/internal/task-artifact.ts +1 -1
  39. package/lib/internal/task-finalization.ts +1 -1
  40. package/lib/internal/task-lifecycle.ts +1 -1
  41. package/lib/internal/task-operation-registry.ts +22 -12
  42. package/lib/internal/task-orchestration.ts +1 -1
  43. package/lib/internal/task-review.ts +9 -5
  44. package/lib/platform/activity-recovery.ts +179 -0
  45. package/lib/platform/issue-comments.ts +98 -31
  46. package/lib/platform/task-comment-projection.ts +79 -0
  47. package/lib/platform/task-recovery.ts +54 -0
  48. package/lib/platform/verification-sync.ts +46 -13
  49. package/lib/sandbox/control/executor.ts +4 -2
  50. package/lib/sandbox/control/protocol.ts +1 -1
  51. package/lib/sandbox/control/task-workflow.ts +1 -1
  52. package/lib/sandbox/control/workflow-executor.ts +102 -21
  53. package/lib/server/process-state.ts +26 -5
  54. package/lib/task/artifact-command.ts +6 -4
  55. package/lib/task/artifact-operations.ts +1 -1
  56. package/lib/task/artifact-recovery.ts +120 -67
  57. package/lib/task/artifact-repair-intent.ts +38 -11
  58. package/lib/task/control-authority.ts +1 -1
  59. package/lib/task/events.ts +6 -6
  60. package/lib/task/intermediate-cleanup.ts +18 -2
  61. package/lib/task/local-artifact-finalization.ts +85 -11
  62. package/lib/task/review-command.ts +1 -1
  63. package/lib/task/review-finalization.ts +203 -8
  64. package/lib/{host-control → task}/secure-fs.ts +10 -32
  65. package/lib/task/workflow-command.ts +2 -0
  66. package/lib/task/workflow-dispatch.ts +171 -0
  67. package/package.json +1 -1
  68. package/templates/.agents/rules/issue-sync.en.md +9 -1
  69. package/templates/.agents/rules/issue-sync.zh-CN.md +10 -3
  70. package/templates/.agents/rules/local-artifact-repair.en.md +7 -6
  71. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +10 -8
  72. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  73. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  74. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  75. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +9 -4
  76. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  77. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  78. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  79. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  80. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  81. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  82. package/dist/lib/host-control/audit.js +0 -19
  83. package/dist/lib/host-control/client.js +0 -142
  84. package/dist/lib/host-control/command.js +0 -49
  85. package/dist/lib/host-control/path.js +0 -160
  86. package/dist/lib/host-control/server.js +0 -159
  87. package/dist/lib/host-control/service.js +0 -112
  88. package/dist/lib/internal/host-control.js +0 -28
  89. package/lib/host-control/audit.ts +0 -22
  90. package/lib/host-control/client.ts +0 -172
  91. package/lib/host-control/command.ts +0 -60
  92. package/lib/host-control/path.ts +0 -167
  93. package/lib/host-control/server.ts +0 -180
  94. package/lib/host-control/service.ts +0 -133
  95. package/lib/internal/host-control.ts +0 -28
@@ -37,22 +37,28 @@ export function isArtifactRecoveryIntent(value) {
37
37
  return false;
38
38
  const intent = value;
39
39
  return exactKeys(intent, [
40
- 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
40
+ 'activeGenerationSemanticDigest', 'activeGenerationSha256', 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
41
41
  'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
42
- 'phase', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
42
+ 'pendingGenerationSemanticDigest', 'pendingGenerationSha256', 'phase', 'preflightArtifactSha256', 'preflightSemanticDigest', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
43
43
  'version'
44
44
  ])
45
- && intent.version === 3
45
+ && intent.version === 4
46
46
  && validTaskId(intent.taskId)
47
47
  && typeof intent.family === 'string'
48
48
  && ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
49
49
  && Number.isSafeInteger(intent.round) && intent.round > 0
50
50
  && validArtifact(intent.artifact, intent.family, intent.round)
51
- && ['awaiting-recovery', 'finalize-ready', 'commit-started', 'passed', 'consumed', 'aborted'].includes(String(intent.state))
51
+ && ['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started', 'passed', 'consumption-started', 'consumed', 'aborted'].includes(String(intent.state))
52
52
  && validDigest(intent.baselineSha256)
53
53
  && validDigest(intent.baselineSemanticDigest)
54
54
  && validRecoveryId(intent.stagingId)
55
55
  && (intent.candidateSha256 === null || validDigest(intent.candidateSha256))
56
+ && (intent.preflightArtifactSha256 === null || validDigest(intent.preflightArtifactSha256))
57
+ && (intent.preflightSemanticDigest === null || validDigest(intent.preflightSemanticDigest))
58
+ && (intent.activeGenerationSha256 === null || validDigest(intent.activeGenerationSha256))
59
+ && (intent.activeGenerationSemanticDigest === null || validDigest(intent.activeGenerationSemanticDigest))
60
+ && (intent.pendingGenerationSha256 === null || validDigest(intent.pendingGenerationSha256))
61
+ && (intent.pendingGenerationSemanticDigest === null || validDigest(intent.pendingGenerationSemanticDigest))
56
62
  && (intent.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
57
63
  && (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
58
64
  && validRecoveryId(intent.recoveryOperationId)
@@ -64,11 +70,22 @@ export function isArtifactRecoveryIntent(value) {
64
70
  && (intent.errorMessage === null || validToken(intent.errorMessage))
65
71
  && Number.isSafeInteger(intent.createdAt)
66
72
  && Number.isSafeInteger(intent.updatedAt)
67
- && (intent.state === 'awaiting-recovery'
68
- ? intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
73
+ && (intent.state === 'awaiting-preflight-recovery'
74
+ ? intent.preflightArtifactSha256 === null && intent.preflightSemanticDigest === null
75
+ && intent.activeGenerationSha256 === null && intent.activeGenerationSemanticDigest === null
76
+ && intent.pendingGenerationSha256 === null && intent.pendingGenerationSemanticDigest === null
77
+ && intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
69
78
  : true)
70
- && (intent.state === 'finalize-ready' || intent.state === 'commit-started' || intent.state === 'passed' || intent.state === 'consumed'
71
- ? validDigest(intent.candidateSha256) && validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
79
+ && (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started' || intent.state === 'preflight-passed' || intent.state === 'full-finalizer-ready' || intent.state === 'commit-started' || intent.state === 'passed' || intent.state === 'consumed'
80
+ ? validDigest(intent.candidateSha256) && validDigest(intent.preflightArtifactSha256) && validDigest(intent.preflightSemanticDigest)
81
+ && validDigest(intent.activeGenerationSha256) && validDigest(intent.activeGenerationSemanticDigest)
82
+ : true)
83
+ && (intent.state === 'full-finalizer-ready' || intent.state === 'commit-started'
84
+ ? validDigest(intent.pendingGenerationSha256) && validDigest(intent.pendingGenerationSemanticDigest)
85
+ && validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
86
+ : true)
87
+ && (intent.state === 'passed' || intent.state === 'consumption-started' || intent.state === 'consumed'
88
+ ? validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
72
89
  : true)
73
90
  && (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
74
91
  }
@@ -266,7 +266,7 @@ function lifecycleRecoveryIntentMatches(intent, selector) {
266
266
  return intent.taskId === selector.taskId
267
267
  && intent.family === selector.family
268
268
  && intent.artifact === selector.artifact
269
- && (intent.state === 'commit-started' || intent.state === 'consumed');
269
+ && (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
270
270
  }
271
271
  /**
272
272
  * Completes only the durable tail of a committed task event after a process
@@ -334,7 +334,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
334
334
  }
335
335
  intent = reconciled.intent;
336
336
  }
337
- if (!['passed', 'consumed'].includes(intent.state)) {
337
+ if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
338
338
  return {
339
339
  code: 'EVENT_ARTIFACT_CONFLICT',
340
340
  message: `review finalizer provenance is incomplete for ${artifact}`
@@ -355,7 +355,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
355
355
  message: `review finalizer provenance does not match ${artifact}`
356
356
  };
357
357
  }
358
- if (intent.state === 'passed') {
358
+ if (intent.state === 'passed' || intent.state === 'consumption-started') {
359
359
  const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
360
360
  writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
361
361
  }
@@ -935,7 +935,7 @@ function applyTaskEventUnlocked(request, options = {}) {
935
935
  if (reconciled.status === 'passed' || reconciled.status === 'consumed')
936
936
  reviewFinalizationIntent = reconciled.intent;
937
937
  }
938
- if (!reviewFinalizationIntent || !['passed', 'consumed'].includes(reviewFinalizationIntent.state)) {
938
+ if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
939
939
  return failed(normalized, {
940
940
  code: 'EVENT_ARTIFACT_CONFLICT',
941
941
  message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
@@ -1106,7 +1106,7 @@ function applyTaskEventUnlocked(request, options = {}) {
1106
1106
  const expectedIntent = localFinalizationIntent;
1107
1107
  localFinalizationIntent = {
1108
1108
  ...localFinalizationIntent,
1109
- state: 'commit-started',
1109
+ state: 'consumption-started',
1110
1110
  phase: 'task-event.completed',
1111
1111
  authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
1112
1112
  requestId: lifecycleAuthority.lifecycleRequestId,
@@ -1138,7 +1138,7 @@ function applyTaskEventUnlocked(request, options = {}) {
1138
1138
  const expectedIntent = reviewFinalizationIntent;
1139
1139
  reviewFinalizationIntent = {
1140
1140
  ...reviewFinalizationIntent,
1141
- state: 'commit-started',
1141
+ state: 'consumption-started',
1142
1142
  phase: 'task-event.completed',
1143
1143
  updatedAt: Date.now()
1144
1144
  };
@@ -1179,7 +1179,7 @@ function applyTaskEventUnlocked(request, options = {}) {
1179
1179
  }
1180
1180
  if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
1181
1181
  try {
1182
- if (reviewFinalizationIntent.state === 'commit-started') {
1182
+ if (reviewFinalizationIntent.state === 'consumption-started') {
1183
1183
  const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
1184
1184
  writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
1185
1185
  }
@@ -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)
@@ -8,7 +8,7 @@ import { expectedQualificationRelations, validateQualificationAudit } from './qu
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),
@@ -136,7 +136,7 @@ function tupleFor(request, taskId, authority) {
136
136
  authorityDigest: authority ? authorityDigest(authority) : null
137
137
  };
138
138
  }
139
- function prepareLocalArtifact(request, candidate, authority) {
139
+ function prepareLocalArtifact(request, candidate, authority, preflightOnly = false) {
140
140
  const failed = (code, message, extra = {}) => ({
141
141
  result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
142
142
  });
@@ -152,14 +152,16 @@ function prepareLocalArtifact(request, candidate, authority) {
152
152
  let taskContent;
153
153
  let content;
154
154
  let recovery;
155
+ let recoveryState;
155
156
  try {
156
157
  taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
157
158
  if (request.recoveryId) {
158
159
  const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
159
160
  if (!intent || intent.recoveryOperationId !== request.recoveryId)
160
161
  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))
162
+ if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
162
163
  return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
164
+ recoveryState = intent.state;
163
165
  recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
164
166
  content = fs.existsSync(recovery.stagingPath)
165
167
  ? fs.readFileSync(recovery.stagingPath, 'utf8')
@@ -192,24 +194,63 @@ function prepareLocalArtifact(request, candidate, authority) {
192
194
  if (request.recoveryId) {
193
195
  if (!validation.ok)
194
196
  return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
197
+ if (preflightOnly && recovery && recoveryState === 'awaiting-preflight-recovery') {
198
+ const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
199
+ prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
200
+ }
195
201
  return {
196
- result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(recovery) },
202
+ result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(recovery) },
197
203
  content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
198
204
  };
199
205
  }
200
206
  const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
201
207
  if (existing?.state === 'passed' || existing?.state === 'consumed') {
202
- if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
208
+ if (existing.state === 'consumed' && (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest)) {
203
209
  return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
204
210
  }
211
+ if (existing.finalArtifactSha256 === artifactSha256 && existing.finalSemanticDigest === validation.semanticDigest) {
212
+ const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
213
+ return {
214
+ result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
215
+ content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
216
+ };
217
+ }
218
+ }
219
+ if (existing && ['preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready'].includes(existing.state)) {
220
+ const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
221
+ return {
222
+ result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
223
+ content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
224
+ };
225
+ }
226
+ if (existing?.state === 'commit-started') {
227
+ if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
228
+ return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match the interrupted recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
229
+ }
205
230
  const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
231
+ const reconciled = reconcileArtifactRecovery(context, {
232
+ lockAlreadyHeld: request.lockAlreadyHeld,
233
+ validateFinal: () => validation.semanticDigest === existing.finalSemanticDigest
234
+ });
235
+ if (reconciled.status !== 'passed') {
236
+ return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `interrupted recovery could not be reconciled: ${reconciled.status}`), content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
237
+ }
206
238
  return {
207
- result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
239
+ result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
208
240
  content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
209
241
  };
210
242
  }
211
243
  try {
212
244
  if (validation.ok) {
245
+ if (preflightOnly) {
246
+ const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
247
+ repoRoot: resolved.repoRoot, taskDir: resolved.taskDir,
248
+ ...(authority ? { recoveryId: authority.operationId } : {}), lockAlreadyHeld: request.lockAlreadyHeld
249
+ });
250
+ const staged = stageArtifactCandidate(context, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
251
+ prepareArtifactRecoveryCommit(context, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
252
+ return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
253
+ }
213
254
  const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
214
255
  repoRoot: resolved.repoRoot,
215
256
  taskDir: resolved.taskDir,
@@ -219,7 +260,7 @@ function prepareLocalArtifact(request, candidate, authority) {
219
260
  lockAlreadyHeld: request.lockAlreadyHeld
220
261
  });
221
262
  recovery = context;
222
- return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
263
+ return { result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
223
264
  }
224
265
  const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
225
266
  repoRoot: resolved.repoRoot,
@@ -237,7 +278,7 @@ function prepareLocalArtifact(request, candidate, authority) {
237
278
  return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', String(error)), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
238
279
  }
239
280
  }
240
- function commitLocalArtifactProvenance(prepared) {
281
+ function commitLocalArtifactProvenance(prepared, options = {}) {
241
282
  if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery)
242
283
  return prepared.result;
243
284
  if (prepared.result.status === 'failed')
@@ -250,30 +291,48 @@ function commitLocalArtifactProvenance(prepared) {
250
291
  if (intent.state === 'passed' || intent.state === 'consumed') {
251
292
  return {
252
293
  ...prepared.result,
294
+ changed: false,
253
295
  artifactSha256: intent.finalArtifactSha256,
254
296
  semanticDigest: intent.finalSemanticDigest,
255
297
  error: null
256
298
  };
257
299
  }
258
- else if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
259
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
300
+ else if (intent.state === 'commit-started') {
301
+ const reconciled = reconcileArtifactRecovery(prepared.recovery, {
302
+ lockAlreadyHeld: prepared.lockAlreadyHeld,
303
+ afterPublish: options.afterPublish
304
+ });
305
+ committed = reconciled.intent;
260
306
  }
261
- else {
307
+ else if (intent.state === 'awaiting-preflight-recovery') {
262
308
  const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
263
309
  prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
264
310
  committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
265
311
  }
266
- if (committed.state !== 'passed')
267
- throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
312
+ if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
313
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
314
+ }
315
+ if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
316
+ prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
317
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
318
+ }
319
+ else if (intent.state === 'full-finalizer-ready') {
320
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
321
+ }
322
+ if (!committed || committed.state !== 'passed')
323
+ throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
268
324
  return {
269
325
  ...prepared.result,
270
326
  status: 'passed',
327
+ changed: false,
271
328
  artifactSha256: committed.finalArtifactSha256,
272
329
  semanticDigest: committed.finalSemanticDigest,
273
330
  error: null
274
331
  };
275
332
  }
276
333
  catch (error) {
334
+ if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
335
+ throw error;
277
336
  return { ...prepared.result, status: 'failed', error: {
278
337
  code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
279
338
  } };
@@ -295,5 +354,10 @@ function finalizeLocalArtifact(request, authority, options = {}) {
295
354
  }
296
355
  return result;
297
356
  }
298
- export { LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, prepareLocalArtifact, commitLocalArtifactProvenance, readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent, canonicalSemanticDigest as semanticDigest, sha256Content, validateLocalArtifact };
357
+ /** Validate and seal an immutable preflight generation without publishing the formal artifact. */
358
+ function preflightLocalArtifact(request, authority) {
359
+ const prepared = prepareLocalArtifact(request, undefined, authority, true);
360
+ return prepared.result;
361
+ }
362
+ export { LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, preflightLocalArtifact, prepareLocalArtifact, commitLocalArtifactProvenance, readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent, canonicalSemanticDigest as semanticDigest, sha256Content, validateLocalArtifact };
299
363
  //# sourceMappingURL=local-artifact-finalization.js.map
@@ -1,6 +1,6 @@
1
1
  export function parseReviewCommand(args) {
2
- if (!args[0] || args[1] !== 'finalize-summary')
3
- throw new Error('task ref and finalize-summary are required');
2
+ if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? ''))
3
+ throw new Error('task ref and a supported review operation are required');
4
4
  const values = {};
5
5
  const flags = {
6
6
  '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',