@fitlab-ai/agent-infra 0.9.15 → 0.10.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 (184) hide show
  1. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  3. package/dist/lib/fs/durable-write.js +2 -0
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/manual-validation.js +17 -8
  6. package/dist/lib/internal/task-artifact.js +1 -1
  7. package/dist/lib/internal/task-lifecycle.js +2 -2
  8. package/dist/lib/internal/task-operation-registry.js +0 -1
  9. package/dist/lib/internal/task-review.js +4 -4
  10. package/dist/lib/platform/issue-comments.js +16 -10
  11. package/dist/lib/platform/issues.js +1 -1
  12. package/dist/lib/platform/pr-change-report.js +5 -4
  13. package/dist/lib/platform/pr-summary.js +64 -32
  14. package/dist/lib/platform/pull-requests.js +3 -3
  15. package/dist/lib/platform/task-identities.js +2 -9
  16. package/dist/lib/platform/verification-sync.js +6 -2
  17. package/dist/lib/pr-review/evidence-grading.js +4 -8
  18. package/dist/lib/sandbox/commands/rm.js +21 -2
  19. package/dist/lib/sandbox/control/server.js +29 -9
  20. package/dist/lib/sandbox/control/state.js +7 -1
  21. package/dist/lib/sandbox/control/workflow-executor.js +4 -14
  22. package/dist/lib/sandbox/index.js +1 -2
  23. package/dist/lib/sandbox/removal.js +116 -1011
  24. package/dist/lib/sandbox/workspace-identity.js +17 -1
  25. package/dist/lib/task/artifact-command.js +6 -28
  26. package/dist/lib/task/artifact-operations.js +16 -230
  27. package/dist/lib/task/artifact-recovery.js +430 -0
  28. package/dist/lib/task/artifact-repair-intent.js +73 -39
  29. package/dist/lib/task/commit-operation.js +31 -4
  30. package/dist/lib/task/control-authority.js +38 -7
  31. package/dist/lib/task/control-recovery.js +14 -2
  32. package/dist/lib/task/create-service.js +1 -8
  33. package/dist/lib/task/delegation-receipts.js +64 -3
  34. package/dist/lib/task/events.js +164 -22
  35. package/dist/lib/task/human-override.js +5 -8
  36. package/dist/lib/task/intermediate-cleanup.js +66 -8
  37. package/dist/lib/task/lifecycle-recovery.js +549 -0
  38. package/dist/lib/task/lifecycle.js +12 -3
  39. package/dist/lib/task/local-artifact-finalization.js +148 -123
  40. package/dist/lib/task/manual-validation-receipt.js +5 -1
  41. package/dist/lib/task/orchestration.js +52 -2
  42. package/dist/lib/task/pr-delivery-fact.js +12 -56
  43. package/dist/lib/task/recovery-warning.js +19 -0
  44. package/dist/lib/task/review-command.js +1 -0
  45. package/dist/lib/task/review-finalization.js +104 -77
  46. package/dist/lib/task/short-id.js +15 -7
  47. package/dist/lib/task/task-execution-lock.js +1 -1
  48. package/dist/lib/task/verification-engine.js +3 -14
  49. package/dist/lib/task/workflow-command.js +0 -1
  50. package/dist/lib/version.js +1 -11
  51. package/dist/lifecycle-build-manifest.json +3 -3
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
  53. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  54. package/lib/fs/durable-write.ts +1 -0
  55. package/lib/internal/cli-route-inventory.ts +1 -1
  56. package/lib/internal/manual-validation.ts +17 -8
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +4 -3
  59. package/lib/internal/task-operation-registry.ts +0 -1
  60. package/lib/internal/task-review.ts +4 -4
  61. package/lib/platform/issue-comments.ts +18 -10
  62. package/lib/platform/issues.ts +1 -1
  63. package/lib/platform/pr-change-report.ts +7 -5
  64. package/lib/platform/pr-summary.ts +64 -32
  65. package/lib/platform/pull-requests.ts +3 -3
  66. package/lib/platform/task-identities.ts +3 -14
  67. package/lib/platform/verification-sync.ts +6 -2
  68. package/lib/pr-review/evidence-grading.ts +4 -9
  69. package/lib/sandbox/commands/rm.ts +34 -2
  70. package/lib/sandbox/control/protocol.ts +5 -0
  71. package/lib/sandbox/control/server.ts +30 -10
  72. package/lib/sandbox/control/state.ts +9 -1
  73. package/lib/sandbox/control/workflow-executor.ts +4 -14
  74. package/lib/sandbox/index.ts +1 -2
  75. package/lib/sandbox/removal.ts +126 -1128
  76. package/lib/sandbox/workspace-identity.ts +19 -3
  77. package/lib/task/artifact-command.ts +8 -27
  78. package/lib/task/artifact-operations.ts +21 -251
  79. package/lib/task/artifact-recovery.ts +519 -0
  80. package/lib/task/artifact-repair-intent.ts +99 -51
  81. package/lib/task/commit-operation.ts +35 -4
  82. package/lib/task/control-authority.ts +47 -14
  83. package/lib/task/control-recovery.ts +13 -1
  84. package/lib/task/create-service.ts +1 -10
  85. package/lib/task/delegation-receipts.ts +73 -2
  86. package/lib/task/events.ts +187 -23
  87. package/lib/task/human-override.ts +5 -8
  88. package/lib/task/intermediate-cleanup.ts +50 -12
  89. package/lib/task/lifecycle-recovery.ts +691 -0
  90. package/lib/task/lifecycle.ts +13 -4
  91. package/lib/task/local-artifact-finalization.ts +176 -132
  92. package/lib/task/manual-validation-receipt.ts +5 -1
  93. package/lib/task/orchestration.ts +65 -0
  94. package/lib/task/pr-delivery-fact.ts +15 -62
  95. package/lib/task/recovery-warning.ts +21 -0
  96. package/lib/task/review-command.ts +1 -0
  97. package/lib/task/review-finalization.ts +106 -87
  98. package/lib/task/short-id.ts +27 -5
  99. package/lib/task/task-execution-lock.ts +1 -1
  100. package/lib/task/verification-engine.ts +3 -17
  101. package/lib/task/verification-types.ts +0 -2
  102. package/lib/task/workflow-command.ts +0 -1
  103. package/lib/version.ts +1 -15
  104. package/package.json +1 -1
  105. package/templates/.agents/rules/create-issue.en.md +1 -1
  106. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  107. package/templates/.agents/rules/issue-sync.en.md +4 -2
  108. package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
  109. package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
  110. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
  111. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  112. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  113. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  114. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  115. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  116. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  117. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  118. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  119. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  120. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  121. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  122. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
  123. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  124. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  125. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  126. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  127. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  130. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  131. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  132. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  133. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  134. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  135. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  136. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  137. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  138. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  139. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  140. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  141. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  142. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  143. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  144. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  145. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  146. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  147. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  148. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  149. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  150. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  151. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  152. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  153. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  156. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  157. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  158. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  159. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  160. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  161. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  162. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  163. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  164. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  165. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  166. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  167. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  168. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  169. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  170. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  171. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  172. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  173. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  174. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  175. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  176. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  177. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  178. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  179. package/templates/.agents/templates/task.en.md +1 -1
  180. package/templates/.agents/templates/task.zh-CN.md +1 -1
  181. package/templates/.claude/commands/import-issue.en.md +1 -1
  182. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  183. package/templates/.claude/commands/restore-task.en.md +1 -1
  184. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
@@ -38,7 +38,9 @@ import {
38
38
  readLocalArtifactFinalizationIntent,
39
39
  validateLocalArtifact
40
40
  } from './local-artifact-finalization.ts';
41
- import { writeArtifactRepairIntent } from './artifact-repair-intent.ts';
41
+ import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
42
+ import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.ts';
43
+ import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
42
44
  import type { LocalArtifactFamily, LocalArtifactFinalizationIntent } from './local-artifact-finalization.ts';
43
45
  import { buildLifecycleFacts, canStart } from './capabilities.ts';
44
46
  import type { ExplicitTrigger, LifecycleAction, TriggerInitiator, TriggerReason } from './capabilities.ts';
@@ -48,7 +50,7 @@ import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation,
48
50
  import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.ts';
49
51
  import type { QualificationAudit, UpstreamRelation } from './qualification-audit.ts';
50
52
  import { getArtifactSchema } from './artifact-schema.ts';
51
- import { inspectArtifactContract } from './artifact-operations.ts';
53
+ import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.ts';
52
54
  import {
53
55
  consumeLifecycleRecoveryAttestation,
54
56
  lifecycleRecoveryAttestationDigest,
@@ -373,6 +375,67 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
373
375
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
374
376
  }
375
377
 
378
+ function reconcileReviewCompletionReplay(
379
+ repoRoot: string,
380
+ taskDir: string,
381
+ taskId: string,
382
+ family: EventFamily,
383
+ artifact: string,
384
+ round: number
385
+ ): TaskEventError | null {
386
+ const stage = REVIEW_LEDGER_STAGES[family];
387
+ if (!stage) return null;
388
+ const reviewFamily = family as 'review-analysis' | 'review-plan' | 'review-code';
389
+ try {
390
+ let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
391
+ if (!intent) return null;
392
+ if (intent.state === 'commit-started') {
393
+ const reconciled = reconcileArtifactRecovery(
394
+ recoveryContextFromIntent(repoRoot, taskDir, intent),
395
+ { lockAlreadyHeld: true }
396
+ );
397
+ if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
398
+ return {
399
+ code: 'EVENT_ARTIFACT_CONFLICT',
400
+ message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
401
+ };
402
+ }
403
+ intent = reconciled.intent;
404
+ }
405
+ if (!['passed', 'consumed'].includes(intent.state)) {
406
+ return {
407
+ code: 'EVENT_ARTIFACT_CONFLICT',
408
+ message: `review finalizer provenance is incomplete for ${artifact}`
409
+ };
410
+ }
411
+ const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
412
+ if (!validated.ok) {
413
+ return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
414
+ }
415
+ const content = fs.readFileSync(validated.artifact.path, 'utf8');
416
+ const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
417
+ if (intent.round !== round
418
+ || intent.requestId !== expectedRequestId
419
+ || intent.finalArtifactSha256 !== sha256Content(content)
420
+ || intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
421
+ return {
422
+ code: 'EVENT_ARTIFACT_CONFLICT',
423
+ message: `review finalizer provenance does not match ${artifact}`
424
+ };
425
+ }
426
+ if (intent.state === 'passed') {
427
+ const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
428
+ writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
429
+ }
430
+ return null;
431
+ } catch (error) {
432
+ return {
433
+ code: 'EVENT_ARTIFACT_CONFLICT',
434
+ message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
435
+ };
436
+ }
437
+ }
438
+
376
439
  function relationForStartedInput(family: EventFamily, input: ArtifactIdentity): UpstreamRelation['relation'] {
377
440
  if (family.startsWith('review-')) {
378
441
  return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
@@ -720,6 +783,8 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
720
783
  const row = manual ? openRows.at(-1) : matchingRows[0];
721
784
  let completedArtifact: ArtifactIdentity | null = null;
722
785
  let localFinalizationIntent: LocalArtifactFinalizationIntent | null = null;
786
+ let reviewFinalizationIntent: ArtifactRecoveryIntent | null = null;
787
+ let reviewContent: string | null = null;
723
788
  if (eventIdentity.phase === 'started' && row) {
724
789
  if (row.agent !== normalized.agent) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
725
790
  if (manual && normalized.transactionId !== undefined) {
@@ -732,7 +797,20 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
732
797
  }
733
798
  if (eventIdentity.phase === 'started' && !manual && completedRows.length > 0) return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
734
799
  const done = completedRows.find((item) => item.note === eventIdentity.note);
735
- if (eventIdentity.phase === 'completed' && done) return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
800
+ if (eventIdentity.phase === 'completed' && done) {
801
+ if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
802
+ const recoveryError = reconcileReviewCompletionReplay(
803
+ resolved.repoRoot,
804
+ resolved.taskDir,
805
+ resolved.taskId,
806
+ eventIdentity.family,
807
+ normalized.artifact,
808
+ normalized.round
809
+ );
810
+ if (recoveryError) return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
811
+ }
812
+ return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
813
+ }
736
814
  if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity is already completed with a different payload' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
737
815
  if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING')) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires one open matching started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
738
816
  if (eventIdentity.phase === 'completed') {
@@ -749,7 +827,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
749
827
  if (validationError) return failed(normalized, validationError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
750
828
  }
751
829
  if (eventIdentity.family.startsWith('review-')) {
752
- let reviewContent: string;
753
830
  try { reviewContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
754
831
  catch (error) {
755
832
  return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot read qualification audit from ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
@@ -776,6 +853,23 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
776
853
  const localFamily = eventIdentity.family === 'analyze'
777
854
  ? 'analysis'
778
855
  : eventIdentity.family === 'plan' ? 'plan' : 'code';
856
+ try {
857
+ localFinalizationIntent = readLocalArtifactFinalizationIntent(
858
+ resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name
859
+ );
860
+ if (localFinalizationIntent?.state === 'commit-started') {
861
+ const reconciled = reconcileArtifactRecovery(
862
+ recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent),
863
+ { lockAlreadyHeld: true }
864
+ );
865
+ if (reconciled.status === 'passed' || reconciled.status === 'consumed') localFinalizationIntent = reconciled.intent;
866
+ }
867
+ } catch (error) {
868
+ return failed(normalized, {
869
+ code: 'EVENT_ARTIFACT_CONFLICT',
870
+ message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
871
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
872
+ }
779
873
  let artifactContent: string;
780
874
  try { artifactContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
781
875
  catch (error) {
@@ -805,34 +899,23 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
805
899
  message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
806
900
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
807
901
  }
808
- try {
809
- localFinalizationIntent = readLocalArtifactFinalizationIntent(
810
- resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name
811
- );
812
- } catch (error) {
813
- return failed(normalized, {
814
- code: 'EVENT_ARTIFACT_CONFLICT',
815
- message: `cannot read local finalizer provenance for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
816
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
817
- }
818
- if (!localFinalizationIntent || !['passed', 'consumed', 'finalize-ready', 'commit-started'].includes(localFinalizationIntent.state)) {
902
+ if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
819
903
  return failed(normalized, {
820
904
  code: 'EVENT_ARTIFACT_CONFLICT',
821
905
  message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
822
906
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
823
907
  }
824
- if (localFinalizationIntent.artifactSha256 !== actualSha256 || localFinalizationIntent.semanticDigest !== local.semanticDigest) {
908
+ if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
825
909
  return failed(normalized, {
826
910
  code: 'EVENT_ARTIFACT_CONFLICT',
827
911
  message: `local finalizer provenance does not match ${completedArtifact.name}`
828
912
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
829
913
  }
830
- if (localFinalizationIntent.recoveryOperationId !== null) {
914
+ if (localFinalizationIntent.authorityDigest !== null) {
831
915
  if (!lifecycleAuthority
832
916
  || localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
833
917
  || localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
834
- || (localFinalizationIntent.phase !== 'artifact.finalize-local'
835
- && localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority))) {
918
+ || localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
836
919
  return failed(normalized, {
837
920
  code: 'EVENT_ARTIFACT_CONFLICT',
838
921
  message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
@@ -865,6 +948,49 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
865
948
  completedArtifact?.path ?? null
866
949
  );
867
950
  if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
951
+ if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
952
+ try {
953
+ reviewFinalizationIntent = readArtifactRecoveryIntent(
954
+ resolved.repoRoot,
955
+ resolved.taskId,
956
+ eventIdentity.family as 'review-analysis' | 'review-plan' | 'review-code',
957
+ completedArtifact.name
958
+ );
959
+ if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
960
+ const reconciled = reconcileArtifactRecovery(
961
+ recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent),
962
+ { lockAlreadyHeld: true }
963
+ );
964
+ if (reconciled.status === 'passed' || reconciled.status === 'consumed') reviewFinalizationIntent = reconciled.intent;
965
+ }
966
+ if (!reviewFinalizationIntent || !['passed', 'consumed'].includes(reviewFinalizationIntent.state)) {
967
+ return failed(normalized, {
968
+ code: 'EVENT_ARTIFACT_CONFLICT',
969
+ message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
970
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
971
+ }
972
+ const reviewStage = eventIdentity.family === 'review-analysis'
973
+ ? 'analysis'
974
+ : eventIdentity.family === 'review-plan' ? 'plan' : 'code';
975
+ const actualSha256 = sha256File(completedArtifact.path);
976
+ const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
977
+ const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
978
+ if (reviewFinalizationIntent.round !== normalized.round
979
+ || reviewFinalizationIntent.requestId !== expectedRequestId
980
+ || reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
981
+ || reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
982
+ return failed(normalized, {
983
+ code: 'EVENT_ARTIFACT_CONFLICT',
984
+ message: `review finalizer provenance does not match ${completedArtifact.name}`
985
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
986
+ }
987
+ } catch (error) {
988
+ return failed(normalized, {
989
+ code: 'EVENT_ARTIFACT_CONFLICT',
990
+ message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
991
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
992
+ }
993
+ }
868
994
  let orchestrationCompletion: OrchestrationStageCompletion | null = null;
869
995
  if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
870
996
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
@@ -996,9 +1122,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
996
1122
  requestId: lifecycleAuthority.lifecycleRequestId,
997
1123
  updatedAt: Date.now()
998
1124
  };
999
- writeArtifactRepairIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
1125
+ writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
1000
1126
  } else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1001
- localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent);
1127
+ localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
1002
1128
  }
1003
1129
  } catch (error) {
1004
1130
  return failed(normalized, {
@@ -1015,6 +1141,31 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1015
1141
  });
1016
1142
  }
1017
1143
  }
1144
+ if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
1145
+ try {
1146
+ const expectedIntent = reviewFinalizationIntent;
1147
+ reviewFinalizationIntent = {
1148
+ ...reviewFinalizationIntent,
1149
+ state: 'commit-started',
1150
+ phase: 'task-event.completed',
1151
+ updatedAt: Date.now()
1152
+ };
1153
+ writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
1154
+ } catch (error) {
1155
+ return failed(normalized, {
1156
+ code: 'EVENT_ARTIFACT_CONFLICT',
1157
+ message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1158
+ }, {
1159
+ taskId: resolved.taskId,
1160
+ taskMdPath: resolved.taskMdPath,
1161
+ fromStep: currentStep,
1162
+ toStep: step,
1163
+ action: eventIdentity.action,
1164
+ phase: eventIdentity.phase,
1165
+ artifactContext
1166
+ });
1167
+ }
1168
+ }
1018
1169
  const sourceCompletion = eventIdentity.phase === 'completed'
1019
1170
  && ['analyze', 'plan', 'code'].includes(eventIdentity.family);
1020
1171
  const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
@@ -1022,7 +1173,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1022
1173
  if (!normalized.dryRun && lifecycleAuthority && localFinalizationIntent && completedArtifact) {
1023
1174
  try {
1024
1175
  if (!options.deferLifecycleRecoveryConsumption) consumeLifecycleRecoveryAttestation(lifecycleAuthority);
1025
- writeArtifactRepairIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
1176
+ writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
1026
1177
  } catch (error) {
1027
1178
  return failed(normalized, {
1028
1179
  code: 'EVENT_ARTIFACT_CONFLICT',
@@ -1030,6 +1181,19 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1030
1181
  }, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
1031
1182
  }
1032
1183
  }
1184
+ if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
1185
+ try {
1186
+ if (reviewFinalizationIntent.state === 'commit-started') {
1187
+ const consumed = { ...reviewFinalizationIntent, state: 'consumed' as const, updatedAt: Date.now() };
1188
+ writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
1189
+ }
1190
+ } catch (error) {
1191
+ return failed(normalized, {
1192
+ code: 'EVENT_ARTIFACT_CONFLICT',
1193
+ message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1194
+ }, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
1195
+ }
1196
+ }
1033
1197
  if (!normalized.dryRun && orchestrationCompletion) {
1034
1198
  try {
1035
1199
  (options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
@@ -1077,7 +1241,7 @@ function applyTaskEvent(request: TaskEventRequest, options: TaskEventOptions = {
1077
1241
  resolved.repoRoot,
1078
1242
  resolved.taskId,
1079
1243
  `task-event.${request.event}`,
1080
- () => applyTaskEventUnlocked(request, options)
1244
+ () => applyTaskEventUnlocked(request, { ...options, lockAlreadyHeld: true })
1081
1245
  );
1082
1246
  } catch (error) {
1083
1247
  if (!(error instanceof TaskExecutionLockError)) throw error;
@@ -617,10 +617,7 @@ function resourceForSnapshot(
617
617
  if (policy.producerId === 'platform.issue') {
618
618
  const identity = options.probeIssueNumber !== undefined
619
619
  ? resourceIdentity(options.probeIssueNumber)
620
- : taskIssueIdentity((frontmatter || {}) as Record<string, string | number | boolean | null>) || (() => {
621
- const number = toPositiveInteger(frontmatter?.issue_number);
622
- return number === null ? null : resourceIdentity(number);
623
- })();
620
+ : taskIssueIdentity((frontmatter || {}) as Record<string, string | number | boolean | null>);
624
621
  return identity ? { kind: 'issue', identity, number: resourceIdentityNumber(identity) } : null;
625
622
  }
626
623
  if (policy.producerId === 'platform.pull-request') {
@@ -719,11 +716,11 @@ async function probePlatformFailure(
719
716
  }
720
717
  const issueIdentity = options.probeIssueNumber !== undefined
721
718
  ? resourceIdentity(options.probeIssueNumber)
722
- : taskIssueIdentity(frontmatter) || (() => {
723
- const number = toPositiveInteger(frontmatter.issue_number);
724
- return number === null ? null : resourceIdentity(number);
725
- })();
719
+ : taskIssueIdentity(frontmatter);
726
720
  const fact = readPrDeliveryFact(frontmatter);
721
+ if (fact.status === 'invalid') {
722
+ return error(fact.error.code, fact.error.message);
723
+ }
727
724
  const pullRequestIdentity = options.probePullRequestNumber !== undefined
728
725
  ? resourceIdentity(options.probePullRequestNumber)
729
726
  : fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
@@ -15,17 +15,18 @@ import {
15
15
  type TaskFinalizationReceipt
16
16
  } from './finalization.ts';
17
17
  import {
18
- readLocalArtifactFinalizationIntent,
19
18
  semanticDigest,
20
- sha256Content,
21
- type LocalArtifactFinalizationIntent
19
+ sha256Content
22
20
  } from './local-artifact-finalization.ts';
21
+ import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
22
+ import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
23
23
  import { parseArtifactName } from './artifact-name.ts';
24
+ import type { ArtifactSchemaFamily } from './artifact-schema.ts';
24
25
  import { enumerateAllTaskDirs, type TaskWorkspaceState } from './resolve-ref.ts';
25
26
  import type { TaskControlBindingEvidence } from './finalization-state.ts';
26
27
 
27
28
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
28
- const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|plan|code)-(.+\.md)\.json$/;
29
+ const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
29
30
  const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
30
31
  const AUXILIARY_ROOTS = [
31
32
  '.local-artifact-finalization-intents',
@@ -161,7 +162,7 @@ function localIntentCandidate(
161
162
  task: TaskRecord | undefined,
162
163
  filePath: string,
163
164
  taskId: string,
164
- family: 'analysis' | 'plan' | 'code',
165
+ family: ArtifactSchemaFamily,
165
166
  artifact: string,
166
167
  identity: FileIdentity,
167
168
  options: ScanOptions
@@ -172,8 +173,8 @@ function localIntentCandidate(
172
173
  || !parseArtifactName(artifact)?.family || parseArtifactName(artifact)!.family !== family) {
173
174
  return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
174
175
  }
175
- let intent: LocalArtifactFinalizationIntent | null;
176
- try { intent = readLocalArtifactFinalizationIntent(repoRoot, taskId, family, artifact); }
176
+ let intent: ArtifactRecoveryIntent | null;
177
+ try { intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact); }
177
178
  catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity); }
178
179
  if (!intent || intent.state !== 'consumed') {
179
180
  return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
@@ -185,7 +186,7 @@ function localIntentCandidate(
185
186
  let content: string;
186
187
  try { content = fs.readFileSync(artifactPath, 'utf8'); }
187
188
  catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity); }
188
- if (intent.artifactSha256 !== sha256Content(content) || intent.semanticDigest !== semanticDigest(content)) {
189
+ if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
189
190
  return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
190
191
  }
191
192
  const receiptReason = receiptGate(repoRoot, task!, options);
@@ -306,7 +307,40 @@ function scanIntermediateCleanup(repoRoot: string, options: ScanOptions = {}): I
306
307
  return buildIntermediateCleanupReport(candidates, true);
307
308
  }
308
309
 
309
- function removeCandidate(candidate: IntermediateCleanupItem): IntermediateCleanupItem {
310
+ function cleanupConsumedRecoveryFiles(repoRoot: string, task: TaskRecord | undefined, candidate: IntermediateCleanupItem): boolean {
311
+ if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId) return true;
312
+ if (!task) return false;
313
+ const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
314
+ if (!match) return false;
315
+ let intent: ReturnType<typeof readArtifactRecoveryIntent>;
316
+ try { intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2] as ArtifactSchemaFamily, match[3]!); }
317
+ catch { return false; }
318
+ if (!intent || intent.state !== 'consumed') return false;
319
+
320
+ const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
321
+ let stat: fs.Stats | null = null;
322
+ try { stat = fs.lstatSync(recovery); } catch (error) {
323
+ if (errorCode(error) === 'ENOENT') return true;
324
+ return false;
325
+ }
326
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
327
+ let entries: fs.Dirent[];
328
+ try { entries = fs.readdirSync(recovery, { withFileTypes: true }); } catch { return false; }
329
+ for (const entry of entries) {
330
+ if (entry.isSymbolicLink() || !entry.isFile() || !['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
331
+ }
332
+ try {
333
+ for (const entry of entries) fs.unlinkSync(path.join(recovery, entry.name));
334
+ fs.rmdirSync(recovery);
335
+ const recoveryRoot = path.dirname(recovery);
336
+ if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
337
+ return true;
338
+ } catch (error) {
339
+ return errorCode(error) === 'ENOENT';
340
+ }
341
+ }
342
+
343
+ function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, task?: TaskRecord): IntermediateCleanupItem {
310
344
  if (candidate.disposition !== 'planned') return candidate;
311
345
  const root = candidate.kind === 'EMPTY-AUX-PARENT'
312
346
  ? path.dirname(path.dirname(candidate.path))
@@ -323,6 +357,9 @@ function removeCandidate(candidate: IntermediateCleanupItem): IntermediateCleanu
323
357
  const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
324
358
  if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
325
359
  if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
360
+ if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
361
+ return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
362
+ }
326
363
  try {
327
364
  if (candidate.kind === 'EMPTY-AUX-PARENT') {
328
365
  if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
@@ -367,6 +404,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
367
404
  indexes.push(index);
368
405
  byTask.set(candidate.taskId, indexes);
369
406
  }
407
+ const tasks = inspectTaskRecords(repoRoot, new Set(byTask.keys()));
370
408
 
371
409
  for (const taskId of [...byTask.keys()].sort()) {
372
410
  const execute = (): void => {
@@ -380,7 +418,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
380
418
  if (initialCandidate.disposition !== 'planned') continue;
381
419
  const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
382
420
  items[index] = current
383
- ? removeCandidate(current)
421
+ ? removeCandidate(repoRoot, current, tasks.get(taskId))
384
422
  : { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
385
423
  }
386
424
  };
@@ -397,14 +435,14 @@ function cleanupIntermediateUnderRemovalCoordinator(
397
435
 
398
436
  for (const [index, candidate] of items.entries()) {
399
437
  if (candidate.taskId || candidate.disposition !== 'planned') continue;
400
- items[index] = removeCandidate(candidate);
438
+ items[index] = removeCandidate(repoRoot, candidate);
401
439
  }
402
440
 
403
441
  const knownPaths = new Set(items.map((candidate) => intermediateCleanupKey(candidate)));
404
442
  for (const candidate of scanIntermediateCleanup(repoRoot, scanOptions).items) {
405
443
  if (candidate.kind !== 'EMPTY-AUX-PARENT' || candidate.disposition !== 'planned'
406
444
  || knownPaths.has(intermediateCleanupKey(candidate))) continue;
407
- items.push(removeCandidate(candidate));
445
+ items.push(removeCandidate(repoRoot, candidate));
408
446
  }
409
447
 
410
448
  return buildIntermediateCleanupReport(items);