@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
@@ -181,6 +181,16 @@ function checkpointIdentity(input, task, delivery) {
181
181
  function checkpointTimestamp() {
182
182
  return new Date().toISOString();
183
183
  }
184
+ function samePreparedCheckpointIdentity(pending, identity) {
185
+ return pending.state === 'prepared'
186
+ && pending.taskId === identity.taskId
187
+ && pending.branch === identity.branch
188
+ && pending.mode === identity.mode
189
+ && pending.expectedHead === identity.expectedHead
190
+ && pending.message === identity.message
191
+ && pending.round === identity.round
192
+ && JSON.stringify(pending.paths) === JSON.stringify(identity.paths);
193
+ }
184
194
  function executeUnlocked(input, task, mode) {
185
195
  const inspected = inspectGitWorkflow(input.cwd);
186
196
  if (!inspected.snapshot)
@@ -214,11 +224,28 @@ function executeUnlocked(input, task, mode) {
214
224
  error: { code: 'COMMIT_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) }
215
225
  };
216
226
  }
217
- if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity))
218
- return {
219
- status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
220
- error: { code: 'COMMIT_INTENT_CONFLICT', message: 'A different checkpoint intent is still pending for this task' }
227
+ if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) {
228
+ if (!input.recoverPreparedIntent || !samePreparedCheckpointIdentity(pendingIntent, identity))
229
+ return {
230
+ status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
231
+ error: { code: 'COMMIT_INTENT_CONFLICT', message: 'A different checkpoint intent is still pending for this task' }
232
+ };
233
+ pendingIntent = {
234
+ ...pendingIntent,
235
+ expectedTree: identity.expectedTree,
236
+ digest: checkpointIntentDigest(identity),
237
+ updatedAt: checkpointTimestamp()
221
238
  };
239
+ try {
240
+ writeCheckpointIntent(task.repoRoot, pendingIntent);
241
+ }
242
+ catch (error) {
243
+ return {
244
+ status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
245
+ error: { code: 'COMMIT_INTENT_WRITE_FAILED', message: error instanceof Error ? error.message : String(error) }
246
+ };
247
+ }
248
+ }
222
249
  if (!pendingIntent) {
223
250
  const timestamp = checkpointTimestamp();
224
251
  pendingIntent = {
@@ -8,12 +8,13 @@ import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegatio
8
8
  import { prepareCodexOrchestrationDelegation } from './codex-orchestration.js';
9
9
  import { applyTaskFinalization } from './finalization.js';
10
10
  import { applyTaskLifecycle, taskLifecycleFailure } from './lifecycle.js';
11
+ import { recoverStartedLifecycleUnderLock, recoveryFailure } from './lifecycle-recovery.js';
11
12
  import { locateActivityLog } from './activity-log.js';
12
13
  import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
13
14
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
14
15
  import { verifyTaskEvent } from './verification.js';
15
16
  import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
16
- import { findArtifactRepairIntentsByOperation, writeArtifactRepairIntent } from './artifact-repair-intent.js';
17
+ import { findArtifactRecoveryIntentsByOperation, writeArtifactRecoveryIntent } from './artifact-repair-intent.js';
17
18
  import { computeLifecycleBuildIdentity, verifyLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
18
19
  const lifecycleAuthorityPhases = [
19
20
  'orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'
@@ -184,7 +185,7 @@ export function queryLifecycleRecoveryOperation(operationId, options = {}) {
184
185
  const store = options.capabilityStore ?? createCodexCapabilityStore();
185
186
  const registry = authorityOperationMap.get(store);
186
187
  const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
187
- const intents = options.repoRoot ? findArtifactRepairIntentsByOperation(options.repoRoot, operationId) : [];
188
+ const intents = options.repoRoot ? findArtifactRecoveryIntentsByOperation(options.repoRoot, operationId) : [];
188
189
  const capabilities = store.findByRecoveryOperation(operationId);
189
190
  const phaseMap = new Map();
190
191
  for (const capability of capabilities) {
@@ -312,19 +313,19 @@ export function recoverLifecycleRecoveryOperation(input, options) {
312
313
  if (!phase) {
313
314
  throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
314
315
  }
315
- const intents = findArtifactRepairIntentsByOperation(options.repoRoot, selector.operationId)
316
+ const intents = findArtifactRecoveryIntentsByOperation(options.repoRoot, selector.operationId)
316
317
  .filter((intent) => intent.taskId === selector.taskId
317
318
  && intent.family === selector.family
318
319
  && intent.artifact === selector.artifact);
319
320
  if (intents.length !== 1) {
320
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not identify this task event');
321
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not identify this task event');
321
322
  }
322
323
  const intent = intents[0];
323
324
  if (!lifecycleRecoveryIntentMatches(intent, selector)
324
325
  || intent.phase !== selector.phase
325
326
  || intent.requestId !== selector.lifecycleRequestId
326
327
  || intent.authorityDigest === null) {
327
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not match the recovery request');
328
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not match the recovery request');
328
329
  }
329
330
  if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
330
331
  throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
@@ -339,7 +340,7 @@ export function recoverLifecycleRecoveryOperation(input, options) {
339
340
  throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
340
341
  }
341
342
  if (intent.state !== 'consumed') {
342
- writeArtifactRepairIntent(options.repoRoot, {
343
+ writeArtifactRecoveryIntent(options.repoRoot, {
343
344
  ...intent,
344
345
  state: 'consumed',
345
346
  updatedAt: (options.now ?? Date.now)()
@@ -473,6 +474,9 @@ async function applyLifecycleWithAuthority(context, request) {
473
474
  return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
474
475
  }
475
476
  const execute = async () => {
477
+ if (request.intent === 'recover-started') {
478
+ return recoverStartedLifecycleUnderLock(request, { repoRoot: context.repoRoot });
479
+ }
476
480
  const lifecycleResult = applyTaskLifecycle(request, { repoRoot: context.repoRoot });
477
481
  if (lifecycleResult.status !== 'failed' || !request.overrideTicket)
478
482
  return lifecycleResult;
@@ -506,6 +510,9 @@ async function applyLifecycleWithAuthority(context, request) {
506
510
  catch (error) {
507
511
  if (!(error instanceof TaskExecutionLockError))
508
512
  throw error;
513
+ if (request.intent === 'recover-started') {
514
+ return recoveryFailure(request, 'owner-unknown', error.code, error.message);
515
+ }
509
516
  return taskLifecycleFailure(request, { code: error.code, message: error.message }, taskId);
510
517
  }
511
518
  }
@@ -616,7 +623,7 @@ function operationInvalid(message) {
616
623
  }
617
624
  const LIFECYCLE_FLAGS = new Set([
618
625
  '--agent', '--reason', '--unblock-condition', '--note', '--alert-number', '--staging-dir', '--issue-number',
619
- '--override-ticket', '--override-target', '--override-scope', '--dry-run'
626
+ '--stage', '--round', '--artifact', '--override-ticket', '--override-target', '--override-scope', '--dry-run'
620
627
  ]);
621
628
  const FINALIZATION_FLAGS = new Set(['--agent']);
622
629
  const ORCHESTRATION_FLAGS = new Set([
@@ -692,8 +699,32 @@ export function parseTaskControlOperation(family, args) {
692
699
  ...(value(values, '--override-scope') ? { overrideScope: value(values, '--override-scope') } : {}),
693
700
  ...(value(values, '--alert-number') ? { alertNumber: Number(value(values, '--alert-number')) } : {}),
694
701
  ...(value(values, '--issue-number') ? { issueNumber: Number(value(values, '--issue-number')) } : {}),
702
+ ...(value(values, '--stage') ? { stage: value(values, '--stage') } : {}),
703
+ ...(value(values, '--round') ? { round: Number(value(values, '--round')) } : {}),
704
+ ...(value(values, '--artifact') ? { artifact: value(values, '--artifact') } : {}),
695
705
  ...(values['--dry-run'] === true ? { dryRun: true } : {})
696
706
  };
707
+ if (intent === 'recover-started') {
708
+ const recoveryFlags = new Set(['--agent', '--stage', '--round', '--artifact', '--reason']);
709
+ for (const flag of Object.keys(values)) {
710
+ if (!recoveryFlags.has(flag))
711
+ operationInvalid(`option '${flag}' is not supported for recover-started`);
712
+ }
713
+ required(values, ['--stage', '--round', '--artifact', '--reason']);
714
+ const stage = value(values, '--stage');
715
+ if (!['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(stage ?? '')) {
716
+ operationInvalid('--stage must be a supported lifecycle stage');
717
+ }
718
+ const round = Number(value(values, '--round'));
719
+ if (!Number.isSafeInteger(round) || round < 1)
720
+ operationInvalid('--round must be a positive integer');
721
+ if (value(values, '--reason').includes('\n') || value(values, '--reason').includes('\r')) {
722
+ operationInvalid('--reason must be a single line');
723
+ }
724
+ }
725
+ else if (values['--stage'] !== undefined || values['--round'] !== undefined || values['--artifact'] !== undefined) {
726
+ operationInvalid('--stage, --round, and --artifact are only supported for recover-started');
727
+ }
697
728
  return { family, request: input };
698
729
  }
699
730
  const values = parseValues(args, 2, ORCHESTRATION_FLAGS);
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { isCompletionEvidence } from './orchestration.js';
3
3
  import { TASK_WORKFLOW_OPERATIONS } from './workflow-command.js';
4
+ import { sameRecoveryWarning } from './recovery-warning.js';
4
5
  /** Normalize the command result once at the control transport boundary. */
5
6
  export function parseControlOutput(output) {
6
7
  if (output === null)
@@ -18,7 +19,7 @@ export function parseControlOutput(output) {
18
19
  export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
19
20
  'completed', 'evidence-written', 'publish-authorized'
20
21
  ]);
21
- const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'];
22
+ const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'];
22
23
  const ORCHESTRATION_INTENTS = [
23
24
  'begin-or-resume', 'route.read', 'route.clean-completion', 'status', 'prepare', 'dispatch',
24
25
  'await-activation', 'recover-prepared', 'hook-start', 'hook-stop', 'advance', 'pause'
@@ -65,6 +66,15 @@ function domainEvidenceMatches(operation, result, domain) {
65
66
  && snapshot.worktreeTree === completion.worktreeTree
66
67
  && lastReviewedCommit === completion.lastReviewedCommit;
67
68
  }
69
+ if (operation.family === 'task-lifecycle' && operation.intent === 'recover-started') {
70
+ const retryRequired = domain.recoveryState === 'retry-required'
71
+ && sameRecoveryWarning(domain.warning, result.warning);
72
+ return result.targetState === 'active'
73
+ && ['applied', 'no-op'].includes(String(result.status))
74
+ && domain.recovery === true
75
+ && domain.targetState === 'active'
76
+ && (domain.recoveryState === 'released' || retryRequired);
77
+ }
68
78
  if (operation.class === 'read-only')
69
79
  return result.changed === false && domain.snapshotValid === true;
70
80
  return true;
@@ -119,7 +129,9 @@ export function classifySandboxControlRecovery(input) {
119
129
  responseReconstructable: true,
120
130
  reasonCode: input.operation.class === 'route.clean-completion' && result.changed === false
121
131
  ? 'RECOVERY_ROUTE_COMPLETION_NOOP'
122
- : 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
132
+ : input.operation.intent === 'recover-started' && input.domain?.recoveryState === 'retry-required'
133
+ ? 'RECOVERY_RELEASE_RETRY_REQUIRED'
134
+ : 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
123
135
  };
124
136
  }
125
137
  export function operationRecoveryBinding(requestId, generation, taskId, family, intent) {
@@ -1,5 +1,3 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
1
  import { syncPlatformComment } from '../platform/issue-comments.js';
4
2
  import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
5
3
  import { VERSION } from '../version.js';
@@ -139,11 +137,6 @@ function platformOperations(prefix, operations) {
139
137
  reasonCode: operation.reasonCode ?? null
140
138
  }));
141
139
  }
142
- function issueFromTask(repoRoot, taskId) {
143
- const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
144
- const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
145
- return match ? { number: Number(match[1]), url: '' } : null;
146
- }
147
140
  async function createTask(value, options) {
148
141
  const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
149
142
  let candidate;
@@ -163,7 +156,7 @@ async function createTask(value, options) {
163
156
  const operations = [{
164
157
  name: 'task:local', status: local.status, reasonCode: null
165
158
  }];
166
- let issue = issueFromTask(options.repoRoot, local.task.id);
159
+ let issue = null;
167
160
  const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
168
161
  operations.push(...platformOperations('platform-create', created.operations));
169
162
  if (created.issue)
@@ -92,7 +92,8 @@ function hasCurrentCodexEvidence(receipt) {
92
92
  const provenance = receipt.lifecycleProvenance;
93
93
  if (!provenance)
94
94
  return false;
95
- const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
95
+ const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status)
96
+ || (receipt.status === 'aborted' && receipt.activatedAt !== null);
96
97
  if (!activated)
97
98
  return receipt.hostEvidence === null;
98
99
  const host = receipt.hostEvidence;
@@ -124,6 +125,12 @@ function hasCurrentCodexEvidence(receipt) {
124
125
  && host.consumer === receipt.id
125
126
  && exactText(host.consumedAt);
126
127
  }
128
+ if (receipt.status === 'aborted') {
129
+ return Number.isSafeInteger(host.stopRevision)
130
+ && host.stopRevision > host.startRevision
131
+ && host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
132
+ && exactText(host.consumedAt);
133
+ }
127
134
  return host.stopRevision === null && host.consumer === null && host.consumedAt === null;
128
135
  }
129
136
  const RECEIPT_KEYS = [
@@ -147,7 +154,8 @@ function hasStatusBoundEvidence(receipt) {
147
154
  const dispatchComplete = dispatchFields.every((field) => field !== null);
148
155
  if (!dispatchEmpty && !dispatchComplete)
149
156
  return false;
150
- const beforeActivation = ['prepared', 'aborted', 'expired'].includes(receipt.status);
157
+ const beforeActivation = ['prepared', 'expired'].includes(receipt.status)
158
+ || (receipt.status === 'aborted' && receipt.activatedAt === null);
151
159
  if (beforeActivation) {
152
160
  return receipt.parentId === null
153
161
  && receipt.childId === null
@@ -181,6 +189,20 @@ function hasStatusBoundEvidence(receipt) {
181
189
  && receipt.sealedAt === null
182
190
  && receipt.consumedAt === null;
183
191
  }
192
+ if (receipt.status === 'aborted') {
193
+ const host = receipt.hostEvidence;
194
+ return receipt.agent === null
195
+ && receipt.afterFingerprint === null
196
+ && receipt.changedPaths.length === 0
197
+ && receipt.sealedAt === null
198
+ && receipt.consumedAt === null
199
+ && host !== null
200
+ && host !== undefined
201
+ && Number.isSafeInteger(host.stopRevision)
202
+ && host.stopRevision > host.startRevision
203
+ && host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
204
+ && exactText(host.consumedAt);
205
+ }
184
206
  if (!exactText(receipt.agent)
185
207
  || normalizeAgentToken(receipt.agent) !== normalizeAgentToken(receipt.client))
186
208
  return false;
@@ -429,6 +451,45 @@ function abortPreparedDelegation(receipt) {
429
451
  receipt: Object.freeze({ ...receipt, status: 'aborted' })
430
452
  };
431
453
  }
454
+ function abortActivatedDelegation(receipt, event) {
455
+ if (receipt.status !== 'activated') {
456
+ return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
457
+ }
458
+ if (receipt.client !== 'codex'
459
+ || !receipt.lifecycleProvenance
460
+ || receipt.hostEvidence?.kind !== 'codex-lifecycle-v2'
461
+ || typeof receipt.lifecycleProvenance.controllerInstanceDigest !== 'string'
462
+ || typeof receipt.lifecycleProvenance.controlGeneration !== 'string'
463
+ || typeof receipt.hostEvidence.controllerInstanceDigest !== 'string'
464
+ || typeof receipt.hostEvidence.controlGeneration !== 'string') {
465
+ return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'activated delegation lacks the current Codex controller binding');
466
+ }
467
+ if (event.childId !== receipt.childId
468
+ || !Number.isSafeInteger(event.stopRevision)
469
+ || event.stopRevision <= receipt.hostEvidence.startRevision
470
+ || event.consumer !== `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
471
+ || !exactText(event.consumedAt)) {
472
+ return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'recovery stop evidence does not match the activated delegation');
473
+ }
474
+ return {
475
+ ok: true,
476
+ receipt: Object.freeze({
477
+ ...receipt,
478
+ status: 'aborted',
479
+ agent: null,
480
+ afterFingerprint: null,
481
+ changedPaths: Object.freeze([]),
482
+ sealedAt: null,
483
+ consumedAt: null,
484
+ hostEvidence: Object.freeze({
485
+ ...receipt.hostEvidence,
486
+ stopRevision: event.stopRevision,
487
+ consumer: event.consumer,
488
+ consumedAt: event.consumedAt
489
+ })
490
+ })
491
+ };
492
+ }
432
493
  function completeDelegationStage(receipt, event) {
433
494
  if (receipt.status !== 'activated')
434
495
  return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
@@ -494,5 +555,5 @@ function consumeDelegation(receipt, options = {}) {
494
555
  consumedAt: (options.now ?? (() => new Date().toISOString()))()
495
556
  }) };
496
557
  }
497
- export { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation };
558
+ export { activateDelegation, abortActivatedDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation };
498
559
  //# sourceMappingURL=delegation-receipts.js.map
@@ -16,13 +16,14 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
16
16
  import { captureTaskWriteMetadata, writeTask } from './write.js';
17
17
  import { allowsManualOverride } from './guard-override.js';
18
18
  import { consumeLocalArtifactFinalizationIntent, readLocalArtifactFinalizationIntent, validateLocalArtifact } from './local-artifact-finalization.js';
19
- import { writeArtifactRepairIntent } from './artifact-repair-intent.js';
19
+ import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
20
+ import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
20
21
  import { buildLifecycleFacts, canStart } from './capabilities.js';
21
22
  import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
22
23
  import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
23
24
  import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.js';
24
25
  import { getArtifactSchema } from './artifact-schema.js';
25
- import { inspectArtifactContract } from './artifact-operations.js';
26
+ import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.js';
26
27
  import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
27
28
  import { readManualValidationCompletion } from './manual-validation-completion.js';
28
29
  const eventCatalog = [
@@ -314,6 +315,59 @@ function openStartedIdentity(rows, family) {
314
315
  function reviewInputFamily(family) {
315
316
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
316
317
  }
318
+ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, artifact, round) {
319
+ const stage = REVIEW_LEDGER_STAGES[family];
320
+ if (!stage)
321
+ return null;
322
+ const reviewFamily = family;
323
+ try {
324
+ let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
325
+ if (!intent)
326
+ return null;
327
+ if (intent.state === 'commit-started') {
328
+ const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(repoRoot, taskDir, intent), { lockAlreadyHeld: true });
329
+ if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
330
+ return {
331
+ code: 'EVENT_ARTIFACT_CONFLICT',
332
+ message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
333
+ };
334
+ }
335
+ intent = reconciled.intent;
336
+ }
337
+ if (!['passed', 'consumed'].includes(intent.state)) {
338
+ return {
339
+ code: 'EVENT_ARTIFACT_CONFLICT',
340
+ message: `review finalizer provenance is incomplete for ${artifact}`
341
+ };
342
+ }
343
+ const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
344
+ if (!validated.ok) {
345
+ return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
346
+ }
347
+ const content = fs.readFileSync(validated.artifact.path, 'utf8');
348
+ const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
349
+ if (intent.round !== round
350
+ || intent.requestId !== expectedRequestId
351
+ || intent.finalArtifactSha256 !== sha256Content(content)
352
+ || intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
353
+ return {
354
+ code: 'EVENT_ARTIFACT_CONFLICT',
355
+ message: `review finalizer provenance does not match ${artifact}`
356
+ };
357
+ }
358
+ if (intent.state === 'passed') {
359
+ const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
360
+ writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
361
+ }
362
+ return null;
363
+ }
364
+ catch (error) {
365
+ return {
366
+ code: 'EVENT_ARTIFACT_CONFLICT',
367
+ message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
368
+ };
369
+ }
370
+ }
317
371
  function relationForStartedInput(family, input) {
318
372
  if (family.startsWith('review-')) {
319
373
  return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
@@ -715,6 +769,8 @@ function applyTaskEventUnlocked(request, options = {}) {
715
769
  const row = manual ? openRows.at(-1) : matchingRows[0];
716
770
  let completedArtifact = null;
717
771
  let localFinalizationIntent = null;
772
+ let reviewFinalizationIntent = null;
773
+ let reviewContent = null;
718
774
  if (eventIdentity.phase === 'started' && row) {
719
775
  if (row.agent !== normalized.agent)
720
776
  return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
@@ -729,8 +785,14 @@ function applyTaskEventUnlocked(request, options = {}) {
729
785
  if (eventIdentity.phase === 'started' && !manual && completedRows.length > 0)
730
786
  return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
731
787
  const done = completedRows.find((item) => item.note === eventIdentity.note);
732
- if (eventIdentity.phase === 'completed' && done)
788
+ if (eventIdentity.phase === 'completed' && done) {
789
+ if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
790
+ const recoveryError = reconcileReviewCompletionReplay(resolved.repoRoot, resolved.taskDir, resolved.taskId, eventIdentity.family, normalized.artifact, normalized.round);
791
+ if (recoveryError)
792
+ return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
793
+ }
733
794
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
795
+ }
734
796
  if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0)
735
797
  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 });
736
798
  if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING'))
@@ -746,7 +808,6 @@ function applyTaskEventUnlocked(request, options = {}) {
746
808
  return failed(normalized, validationError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
747
809
  }
748
810
  if (eventIdentity.family.startsWith('review-')) {
749
- let reviewContent;
750
811
  try {
751
812
  reviewContent = fs.readFileSync(completedArtifact.path, 'utf8');
752
813
  }
@@ -775,6 +836,20 @@ function applyTaskEventUnlocked(request, options = {}) {
775
836
  const localFamily = eventIdentity.family === 'analyze'
776
837
  ? 'analysis'
777
838
  : eventIdentity.family === 'plan' ? 'plan' : 'code';
839
+ try {
840
+ localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
841
+ if (localFinalizationIntent?.state === 'commit-started') {
842
+ const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent), { lockAlreadyHeld: true });
843
+ if (reconciled.status === 'passed' || reconciled.status === 'consumed')
844
+ localFinalizationIntent = reconciled.intent;
845
+ }
846
+ }
847
+ catch (error) {
848
+ return failed(normalized, {
849
+ code: 'EVENT_ARTIFACT_CONFLICT',
850
+ message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
851
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
852
+ }
778
853
  let artifactContent;
779
854
  try {
780
855
  artifactContent = fs.readFileSync(completedArtifact.path, 'utf8');
@@ -806,33 +881,23 @@ function applyTaskEventUnlocked(request, options = {}) {
806
881
  message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
807
882
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
808
883
  }
809
- try {
810
- localFinalizationIntent = readLocalArtifactFinalizationIntent(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)) {
884
+ if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
819
885
  return failed(normalized, {
820
886
  code: 'EVENT_ARTIFACT_CONFLICT',
821
887
  message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
822
888
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
823
889
  }
824
- if (localFinalizationIntent.artifactSha256 !== actualSha256 || localFinalizationIntent.semanticDigest !== local.semanticDigest) {
890
+ if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
825
891
  return failed(normalized, {
826
892
  code: 'EVENT_ARTIFACT_CONFLICT',
827
893
  message: `local finalizer provenance does not match ${completedArtifact.name}`
828
894
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
829
895
  }
830
- if (localFinalizationIntent.recoveryOperationId !== null) {
896
+ if (localFinalizationIntent.authorityDigest !== null) {
831
897
  if (!lifecycleAuthority
832
898
  || localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
833
899
  || localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
834
- || (localFinalizationIntent.phase !== 'artifact.finalize-local'
835
- && localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority))) {
900
+ || localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
836
901
  return failed(normalized, {
837
902
  code: 'EVENT_ARTIFACT_CONFLICT',
838
903
  message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
@@ -862,6 +927,43 @@ function applyTaskEventUnlocked(request, options = {}) {
862
927
  const findingCountError = validateReviewFindingCounts(normalized, content, eventIdentity.family, completedArtifact?.path ?? null);
863
928
  if (findingCountError)
864
929
  return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
930
+ if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
931
+ try {
932
+ reviewFinalizationIntent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, eventIdentity.family, completedArtifact.name);
933
+ if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
934
+ const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent), { lockAlreadyHeld: true });
935
+ if (reconciled.status === 'passed' || reconciled.status === 'consumed')
936
+ reviewFinalizationIntent = reconciled.intent;
937
+ }
938
+ if (!reviewFinalizationIntent || !['passed', 'consumed'].includes(reviewFinalizationIntent.state)) {
939
+ return failed(normalized, {
940
+ code: 'EVENT_ARTIFACT_CONFLICT',
941
+ message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
942
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
943
+ }
944
+ const reviewStage = eventIdentity.family === 'review-analysis'
945
+ ? 'analysis'
946
+ : eventIdentity.family === 'review-plan' ? 'plan' : 'code';
947
+ const actualSha256 = sha256File(completedArtifact.path);
948
+ const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
949
+ const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
950
+ if (reviewFinalizationIntent.round !== normalized.round
951
+ || reviewFinalizationIntent.requestId !== expectedRequestId
952
+ || reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
953
+ || reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
954
+ return failed(normalized, {
955
+ code: 'EVENT_ARTIFACT_CONFLICT',
956
+ message: `review finalizer provenance does not match ${completedArtifact.name}`
957
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
958
+ }
959
+ }
960
+ catch (error) {
961
+ return failed(normalized, {
962
+ code: 'EVENT_ARTIFACT_CONFLICT',
963
+ message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
964
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
965
+ }
966
+ }
865
967
  let orchestrationCompletion = null;
866
968
  if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
867
969
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
@@ -1010,10 +1112,10 @@ function applyTaskEventUnlocked(request, options = {}) {
1010
1112
  requestId: lifecycleAuthority.lifecycleRequestId,
1011
1113
  updatedAt: Date.now()
1012
1114
  };
1013
- writeArtifactRepairIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
1115
+ writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
1014
1116
  }
1015
1117
  else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1016
- localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent);
1118
+ localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
1017
1119
  }
1018
1120
  }
1019
1121
  catch (error) {
@@ -1031,6 +1133,32 @@ function applyTaskEventUnlocked(request, options = {}) {
1031
1133
  });
1032
1134
  }
1033
1135
  }
1136
+ if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
1137
+ try {
1138
+ const expectedIntent = reviewFinalizationIntent;
1139
+ reviewFinalizationIntent = {
1140
+ ...reviewFinalizationIntent,
1141
+ state: 'commit-started',
1142
+ phase: 'task-event.completed',
1143
+ updatedAt: Date.now()
1144
+ };
1145
+ writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
1146
+ }
1147
+ catch (error) {
1148
+ return failed(normalized, {
1149
+ code: 'EVENT_ARTIFACT_CONFLICT',
1150
+ message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1151
+ }, {
1152
+ taskId: resolved.taskId,
1153
+ taskMdPath: resolved.taskMdPath,
1154
+ fromStep: currentStep,
1155
+ toStep: step,
1156
+ action: eventIdentity.action,
1157
+ phase: eventIdentity.phase,
1158
+ artifactContext
1159
+ });
1160
+ }
1161
+ }
1034
1162
  const sourceCompletion = eventIdentity.phase === 'completed'
1035
1163
  && ['analyze', 'plan', 'code'].includes(eventIdentity.family);
1036
1164
  const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
@@ -1040,7 +1168,7 @@ function applyTaskEventUnlocked(request, options = {}) {
1040
1168
  try {
1041
1169
  if (!options.deferLifecycleRecoveryConsumption)
1042
1170
  consumeLifecycleRecoveryAttestation(lifecycleAuthority);
1043
- writeArtifactRepairIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
1171
+ writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
1044
1172
  }
1045
1173
  catch (error) {
1046
1174
  return failed(normalized, {
@@ -1049,6 +1177,20 @@ function applyTaskEventUnlocked(request, options = {}) {
1049
1177
  }, { 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 });
1050
1178
  }
1051
1179
  }
1180
+ if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
1181
+ try {
1182
+ if (reviewFinalizationIntent.state === 'commit-started') {
1183
+ const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
1184
+ writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
1185
+ }
1186
+ }
1187
+ catch (error) {
1188
+ return failed(normalized, {
1189
+ code: 'EVENT_ARTIFACT_CONFLICT',
1190
+ message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1191
+ }, { 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 });
1192
+ }
1193
+ }
1052
1194
  if (!normalized.dryRun && orchestrationCompletion) {
1053
1195
  try {
1054
1196
  (options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
@@ -1095,7 +1237,7 @@ function applyTaskEvent(request, options = {}) {
1095
1237
  if (!resolved.ok)
1096
1238
  return applyTaskEventUnlocked(request, options);
1097
1239
  try {
1098
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => applyTaskEventUnlocked(request, options));
1240
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => applyTaskEventUnlocked(request, { ...options, lockAlreadyHeld: true }));
1099
1241
  }
1100
1242
  catch (error) {
1101
1243
  if (!(error instanceof TaskExecutionLockError))