@fitlab-ai/agent-infra 0.9.15 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  7. package/dist/lib/fs/durable-write.js +2 -0
  8. package/dist/lib/internal/cli-route-inventory.js +3 -4
  9. package/dist/lib/internal/manual-validation.js +17 -8
  10. package/dist/lib/internal/platform-comment.js +60 -2
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-finalization.js +2 -2
  13. package/dist/lib/internal/task-lifecycle.js +4 -4
  14. package/dist/lib/internal/task-operation-registry.js +22 -13
  15. package/dist/lib/internal/task-orchestration.js +2 -2
  16. package/dist/lib/internal/task-review.js +12 -7
  17. package/dist/lib/platform/activity-recovery.js +159 -0
  18. package/dist/lib/platform/issue-comments.js +89 -31
  19. package/dist/lib/platform/issues.js +1 -1
  20. package/dist/lib/platform/pr-change-report.js +5 -4
  21. package/dist/lib/platform/pr-summary.js +64 -32
  22. package/dist/lib/platform/pull-requests.js +3 -3
  23. package/dist/lib/platform/task-comment-projection.js +70 -0
  24. package/dist/lib/platform/task-identities.js +2 -9
  25. package/dist/lib/platform/task-recovery.js +55 -0
  26. package/dist/lib/platform/verification-sync.js +45 -7
  27. package/dist/lib/pr-review/evidence-grading.js +4 -8
  28. package/dist/lib/sandbox/commands/rm.js +21 -2
  29. package/dist/lib/sandbox/control/executor.js +4 -2
  30. package/dist/lib/sandbox/control/server.js +29 -9
  31. package/dist/lib/sandbox/control/state.js +7 -1
  32. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  33. package/dist/lib/sandbox/control/workflow-executor.js +87 -35
  34. package/dist/lib/sandbox/index.js +1 -2
  35. package/dist/lib/sandbox/removal.js +116 -1011
  36. package/dist/lib/sandbox/workspace-identity.js +17 -1
  37. package/dist/lib/server/process-state.js +17 -5
  38. package/dist/lib/task/artifact-command.js +11 -30
  39. package/dist/lib/task/artifact-operations.js +17 -231
  40. package/dist/lib/task/artifact-recovery.js +476 -0
  41. package/dist/lib/task/artifact-repair-intent.js +90 -39
  42. package/dist/lib/task/commit-operation.js +31 -4
  43. package/dist/lib/task/control-authority.js +39 -8
  44. package/dist/lib/task/control-recovery.js +14 -2
  45. package/dist/lib/task/create-service.js +1 -8
  46. package/dist/lib/task/delegation-receipts.js +64 -3
  47. package/dist/lib/task/events.js +165 -23
  48. package/dist/lib/task/human-override.js +5 -8
  49. package/dist/lib/task/intermediate-cleanup.js +91 -8
  50. package/dist/lib/task/lifecycle-recovery.js +549 -0
  51. package/dist/lib/task/lifecycle.js +12 -3
  52. package/dist/lib/task/local-artifact-finalization.js +212 -123
  53. package/dist/lib/task/manual-validation-receipt.js +5 -1
  54. package/dist/lib/task/orchestration.js +52 -2
  55. package/dist/lib/task/pr-delivery-fact.js +12 -56
  56. package/dist/lib/task/recovery-warning.js +19 -0
  57. package/dist/lib/task/review-command.js +3 -2
  58. package/dist/lib/task/review-finalization.js +294 -74
  59. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  60. package/dist/lib/task/short-id.js +15 -7
  61. package/dist/lib/task/task-execution-lock.js +1 -1
  62. package/dist/lib/task/verification-engine.js +3 -14
  63. package/dist/lib/task/workflow-command.js +2 -1
  64. package/dist/lib/task/workflow-dispatch.js +161 -0
  65. package/dist/lib/version.js +1 -11
  66. package/dist/lifecycle-build-manifest.json +3 -3
  67. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
  68. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  69. package/lib/fs/durable-write.ts +1 -0
  70. package/lib/internal/cli-route-inventory.ts +3 -4
  71. package/lib/internal/manual-validation.ts +17 -8
  72. package/lib/internal/platform-comment.ts +53 -2
  73. package/lib/internal/task-artifact.ts +1 -1
  74. package/lib/internal/task-finalization.ts +1 -1
  75. package/lib/internal/task-lifecycle.ts +5 -4
  76. package/lib/internal/task-operation-registry.ts +22 -13
  77. package/lib/internal/task-orchestration.ts +1 -1
  78. package/lib/internal/task-review.ts +11 -7
  79. package/lib/platform/activity-recovery.ts +179 -0
  80. package/lib/platform/issue-comments.ts +105 -30
  81. package/lib/platform/issues.ts +1 -1
  82. package/lib/platform/pr-change-report.ts +7 -5
  83. package/lib/platform/pr-summary.ts +64 -32
  84. package/lib/platform/pull-requests.ts +3 -3
  85. package/lib/platform/task-comment-projection.ts +79 -0
  86. package/lib/platform/task-identities.ts +3 -14
  87. package/lib/platform/task-recovery.ts +54 -0
  88. package/lib/platform/verification-sync.ts +48 -11
  89. package/lib/pr-review/evidence-grading.ts +4 -9
  90. package/lib/sandbox/commands/rm.ts +34 -2
  91. package/lib/sandbox/control/executor.ts +4 -2
  92. package/lib/sandbox/control/protocol.ts +6 -1
  93. package/lib/sandbox/control/server.ts +30 -10
  94. package/lib/sandbox/control/state.ts +9 -1
  95. package/lib/sandbox/control/task-workflow.ts +1 -1
  96. package/lib/sandbox/control/workflow-executor.ts +106 -35
  97. package/lib/sandbox/index.ts +1 -2
  98. package/lib/sandbox/removal.ts +126 -1128
  99. package/lib/sandbox/workspace-identity.ts +19 -3
  100. package/lib/server/process-state.ts +26 -5
  101. package/lib/task/artifact-command.ts +12 -29
  102. package/lib/task/artifact-operations.ts +22 -252
  103. package/lib/task/artifact-recovery.ts +572 -0
  104. package/lib/task/artifact-repair-intent.ts +126 -51
  105. package/lib/task/commit-operation.ts +35 -4
  106. package/lib/task/control-authority.ts +48 -15
  107. package/lib/task/control-recovery.ts +13 -1
  108. package/lib/task/create-service.ts +1 -10
  109. package/lib/task/delegation-receipts.ts +73 -2
  110. package/lib/task/events.ts +188 -24
  111. package/lib/task/human-override.ts +5 -8
  112. package/lib/task/intermediate-cleanup.ts +66 -12
  113. package/lib/task/lifecycle-recovery.ts +691 -0
  114. package/lib/task/lifecycle.ts +13 -4
  115. package/lib/task/local-artifact-finalization.ts +249 -131
  116. package/lib/task/manual-validation-receipt.ts +5 -1
  117. package/lib/task/orchestration.ts +65 -0
  118. package/lib/task/pr-delivery-fact.ts +15 -62
  119. package/lib/task/recovery-warning.ts +21 -0
  120. package/lib/task/review-command.ts +2 -1
  121. package/lib/task/review-finalization.ts +298 -84
  122. package/lib/{host-control → task}/secure-fs.ts +10 -32
  123. package/lib/task/short-id.ts +27 -5
  124. package/lib/task/task-execution-lock.ts +1 -1
  125. package/lib/task/verification-engine.ts +3 -17
  126. package/lib/task/verification-types.ts +0 -2
  127. package/lib/task/workflow-command.ts +2 -1
  128. package/lib/task/workflow-dispatch.ts +171 -0
  129. package/lib/version.ts +1 -15
  130. package/package.json +1 -1
  131. package/templates/.agents/rules/create-issue.en.md +1 -1
  132. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  133. package/templates/.agents/rules/issue-sync.en.md +12 -2
  134. package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
  135. package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
  136. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -36
  137. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  138. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  139. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  140. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  141. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  142. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  143. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  144. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  145. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  146. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  147. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  148. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +8 -8
  149. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  150. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  151. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  152. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  153. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  154. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  155. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  156. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  157. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  158. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  159. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  160. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  161. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  162. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  163. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  164. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  165. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  166. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  167. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  168. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  169. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  170. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  171. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  172. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  173. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  174. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  175. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  176. package/templates/.agents/skills/restore-task/SKILL.en.md +11 -6
  177. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
  178. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  179. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  180. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  181. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  182. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  183. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  184. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  185. package/templates/.agents/skills/review-code/SKILL.en.md +5 -4
  186. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
  187. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  188. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  189. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  190. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  191. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  192. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  193. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  194. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  195. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  196. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  197. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  198. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  199. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  200. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  201. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  202. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  203. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  204. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  205. package/templates/.agents/templates/task.en.md +1 -1
  206. package/templates/.agents/templates/task.zh-CN.md +1 -1
  207. package/templates/.claude/commands/import-issue.en.md +1 -1
  208. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  209. package/templates/.claude/commands/restore-task.en.md +1 -1
  210. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
  211. package/dist/lib/host-control/audit.js +0 -19
  212. package/dist/lib/host-control/client.js +0 -142
  213. package/dist/lib/host-control/command.js +0 -49
  214. package/dist/lib/host-control/path.js +0 -160
  215. package/dist/lib/host-control/server.js +0 -159
  216. package/dist/lib/host-control/service.js +0 -112
  217. package/dist/lib/internal/host-control.js +0 -28
  218. package/lib/host-control/audit.ts +0 -22
  219. package/lib/host-control/client.ts +0 -172
  220. package/lib/host-control/command.ts +0 -60
  221. package/lib/host-control/path.ts +0 -167
  222. package/lib/host-control/server.ts +0 -180
  223. package/lib/host-control/service.ts +0 -133
  224. package/lib/internal/host-control.ts +0 -28
@@ -1,11 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
- function intentRoot(repoRoot) {
4
+ import { parseArtifactName } from './artifact-name.js';
5
+ export function artifactRecoveryIntentRoot(repoRoot) {
5
6
  return path.join(repoRoot, '.agents', 'workspace', '.local-artifact-finalization-intents');
6
7
  }
8
+ export function artifactRecoveryRoot(taskDir, stagingId) {
9
+ return path.join(taskDir, '.local-artifact-recovery', stagingId);
10
+ }
7
11
  function intentPath(repoRoot, taskId, family, artifact) {
8
- return path.join(intentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
12
+ return path.join(artifactRecoveryIntentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
9
13
  }
10
14
  function validDigest(value) {
11
15
  return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
@@ -13,35 +17,82 @@ function validDigest(value) {
13
17
  function validTaskId(value) {
14
18
  return typeof value === 'string' && /^TASK-\d{8}-\d{6}$/u.test(value);
15
19
  }
20
+ function validToken(value) {
21
+ return typeof value === 'string' && value.length > 0 && !/[\r\n]/u.test(value);
22
+ }
23
+ function validRecoveryId(value) {
24
+ return typeof value === 'string' && /^[a-f0-9-]{16,64}$/u.test(value);
25
+ }
26
+ function validArtifact(value, family, round) {
27
+ if (typeof value !== 'string' || path.basename(value) !== value || !value.endsWith('.md'))
28
+ return false;
29
+ const parsed = parseArtifactName(value);
30
+ return Boolean(parsed && parsed.family === family && parsed.round === round);
31
+ }
16
32
  function exactKeys(value, keys) {
17
33
  return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
18
34
  }
19
- export function isArtifactRepairIntent(value) {
35
+ export function isArtifactRecoveryIntent(value) {
20
36
  if (!value || typeof value !== 'object' || Array.isArray(value))
21
37
  return false;
22
38
  const intent = value;
23
39
  return exactKeys(intent, [
24
- 'artifact', 'artifactSha256', 'authorityDigest', 'baselineSemanticDigest', 'createdAt',
25
- 'family', 'phase', 'recoveryOperationId', 'requestId', 'semanticDigest', 'state',
26
- 'taskId', 'updatedAt', 'version'
40
+ 'activeGenerationSemanticDigest', 'activeGenerationSha256', 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
41
+ 'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
42
+ 'pendingGenerationSemanticDigest', 'pendingGenerationSha256', 'phase', 'preflightArtifactSha256', 'preflightSemanticDigest', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
43
+ 'version'
27
44
  ])
28
- && intent.version === 2
45
+ && intent.version === 4
29
46
  && validTaskId(intent.taskId)
30
47
  && typeof intent.family === 'string'
31
48
  && ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
32
- && typeof intent.artifact === 'string' && intent.artifact.length > 0
33
- && ['awaiting-repair', 'finalize-ready', 'commit-started', 'passed', 'consumed'].includes(String(intent.state))
34
- && (intent.baselineSemanticDigest === null || validDigest(intent.baselineSemanticDigest))
35
- && validDigest(intent.artifactSha256)
36
- && validDigest(intent.semanticDigest)
37
- && (intent.recoveryOperationId === null || (typeof intent.recoveryOperationId === 'string' && /^[a-f0-9-]{16,64}$/u.test(intent.recoveryOperationId)))
49
+ && Number.isSafeInteger(intent.round) && intent.round > 0
50
+ && validArtifact(intent.artifact, intent.family, intent.round)
51
+ && ['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started', 'passed', 'consumption-started', 'consumed', 'aborted'].includes(String(intent.state))
52
+ && validDigest(intent.baselineSha256)
53
+ && validDigest(intent.baselineSemanticDigest)
54
+ && validRecoveryId(intent.stagingId)
55
+ && (intent.candidateSha256 === null || validDigest(intent.candidateSha256))
56
+ && (intent.preflightArtifactSha256 === null || validDigest(intent.preflightArtifactSha256))
57
+ && (intent.preflightSemanticDigest === null || validDigest(intent.preflightSemanticDigest))
58
+ && (intent.activeGenerationSha256 === null || validDigest(intent.activeGenerationSha256))
59
+ && (intent.activeGenerationSemanticDigest === null || validDigest(intent.activeGenerationSemanticDigest))
60
+ && (intent.pendingGenerationSha256 === null || validDigest(intent.pendingGenerationSha256))
61
+ && (intent.pendingGenerationSemanticDigest === null || validDigest(intent.pendingGenerationSemanticDigest))
62
+ && (intent.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
63
+ && (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
64
+ && validRecoveryId(intent.recoveryOperationId)
65
+ && intent.stagingId === intent.recoveryOperationId
38
66
  && (intent.phase === null || ['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(String(intent.phase)))
39
67
  && (intent.authorityDigest === null || validDigest(intent.authorityDigest))
40
- && typeof intent.requestId === 'string' && intent.requestId.length > 0 && !/[\r\n]/u.test(intent.requestId)
41
- && Number.isSafeInteger(intent.createdAt) && Number.isSafeInteger(intent.updatedAt)
42
- && (intent.state === 'awaiting-repair' ? intent.baselineSemanticDigest === intent.semanticDigest : true);
68
+ && validToken(intent.requestId)
69
+ && (intent.errorCode === null || validToken(intent.errorCode))
70
+ && (intent.errorMessage === null || validToken(intent.errorMessage))
71
+ && Number.isSafeInteger(intent.createdAt)
72
+ && Number.isSafeInteger(intent.updatedAt)
73
+ && (intent.state === 'awaiting-preflight-recovery'
74
+ ? intent.preflightArtifactSha256 === null && intent.preflightSemanticDigest === null
75
+ && intent.activeGenerationSha256 === null && intent.activeGenerationSemanticDigest === null
76
+ && intent.pendingGenerationSha256 === null && intent.pendingGenerationSemanticDigest === null
77
+ && intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
78
+ : true)
79
+ && (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started' || intent.state === 'preflight-passed' || intent.state === 'full-finalizer-ready' || intent.state === 'commit-started' || intent.state === 'passed' || intent.state === 'consumed'
80
+ ? validDigest(intent.candidateSha256) && validDigest(intent.preflightArtifactSha256) && validDigest(intent.preflightSemanticDigest)
81
+ && validDigest(intent.activeGenerationSha256) && validDigest(intent.activeGenerationSemanticDigest)
82
+ : true)
83
+ && (intent.state === 'full-finalizer-ready' || intent.state === 'commit-started'
84
+ ? validDigest(intent.pendingGenerationSha256) && validDigest(intent.pendingGenerationSemanticDigest)
85
+ && validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
86
+ : true)
87
+ && (intent.state === 'passed' || intent.state === 'consumption-started' || intent.state === 'consumed'
88
+ ? validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
89
+ : true)
90
+ && (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
43
91
  }
44
- function readArtifactRepairIntent(repoRoot, taskId, family, artifact) {
92
+ export function readArtifactRecoveryIntent(repoRoot, taskId, family, artifact) {
93
+ if (!validTaskId(taskId) || !validArtifact(artifact, family, parseArtifactName(artifact)?.round)) {
94
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal identity is invalid');
95
+ }
45
96
  const target = intentPath(repoRoot, taskId, family, artifact);
46
97
  if (!fs.existsSync(target))
47
98
  return null;
@@ -50,19 +101,20 @@ function readArtifactRepairIntent(repoRoot, taskId, family, artifact) {
50
101
  value = JSON.parse(fs.readFileSync(target, 'utf8'));
51
102
  }
52
103
  catch (error) {
53
- throw new Error(`ARTIFACT_REPAIR_INTENT_INVALID: ${String(error)}`);
104
+ throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`);
54
105
  }
55
- if (!isArtifactRepairIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
56
- throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
106
+ if (!isArtifactRecoveryIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
107
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
57
108
  }
58
109
  return value;
59
110
  }
60
- export function writeArtifactRepairIntent(repoRoot, value, options = {}) {
61
- if (!isArtifactRepairIntent(value))
62
- throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
63
- const directory = intentRoot(repoRoot);
111
+ export function writeArtifactRecoveryIntent(repoRoot, value, options = {}) {
112
+ if (!isArtifactRecoveryIntent(value))
113
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
114
+ const intent = value;
115
+ const directory = artifactRecoveryIntentRoot(repoRoot);
64
116
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
65
- const target = intentPath(repoRoot, value.taskId, value.family, value.artifact);
117
+ const target = intentPath(repoRoot, intent.taskId, intent.family, intent.artifact);
66
118
  const lock = `${target}.lock`;
67
119
  let descriptor;
68
120
  try {
@@ -70,19 +122,19 @@ export function writeArtifactRepairIntent(repoRoot, value, options = {}) {
70
122
  }
71
123
  catch (error) {
72
124
  if (error.code === 'EEXIST') {
73
- throw new Error('ARTIFACT_REPAIR_INTENT_CONFLICT: another provenance writer owns the intent');
125
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: another journal writer owns the intent');
74
126
  }
75
127
  throw error;
76
128
  }
77
129
  try {
78
130
  const current = fs.existsSync(target)
79
- ? readArtifactRepairIntent(repoRoot, value.taskId, value.family, value.artifact)
131
+ ? readArtifactRecoveryIntent(repoRoot, intent.taskId, intent.family, intent.artifact)
80
132
  : null;
81
133
  if (!isDeepStrictEqual(current, options.expected ?? null)) {
82
- throw new Error('ARTIFACT_REPAIR_INTENT_CONFLICT: repair provenance changed during finalization');
134
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: recovery journal changed during finalization');
83
135
  }
84
136
  const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
85
- fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
137
+ fs.writeFileSync(temporary, `${JSON.stringify(intent)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
86
138
  try {
87
139
  fs.renameSync(temporary, target);
88
140
  }
@@ -90,7 +142,7 @@ export function writeArtifactRepairIntent(repoRoot, value, options = {}) {
90
142
  try {
91
143
  fs.unlinkSync(temporary);
92
144
  }
93
- catch { /* preserve primary error */ }
145
+ catch { /* preserve the primary error */ }
94
146
  throw error;
95
147
  }
96
148
  }
@@ -102,11 +154,10 @@ export function writeArtifactRepairIntent(repoRoot, value, options = {}) {
102
154
  catch { /* preserve the primary result */ }
103
155
  }
104
156
  }
105
- export function findArtifactRepairIntentsByOperation(repoRoot, operationId) {
106
- if (!operationId || /[\r\n]/u.test(operationId)) {
107
- throw new Error('ARTIFACT_REPAIR_OPERATION_INVALID: recovery operation id is invalid');
108
- }
109
- const directory = intentRoot(repoRoot);
157
+ export function findArtifactRecoveryIntentsByOperation(repoRoot, operationId) {
158
+ if (!validToken(operationId))
159
+ throw new Error('ARTIFACT_RECOVERY_OPERATION_INVALID: recovery operation id is invalid');
160
+ const directory = artifactRecoveryIntentRoot(repoRoot);
110
161
  if (!fs.existsSync(directory))
111
162
  return Object.freeze([]);
112
163
  return Object.freeze(fs.readdirSync(directory)
@@ -118,13 +169,13 @@ export function findArtifactRepairIntentsByOperation(repoRoot, operationId) {
118
169
  value = JSON.parse(fs.readFileSync(target, 'utf8'));
119
170
  }
120
171
  catch (error) {
121
- throw new Error(`ARTIFACT_REPAIR_INTENT_INVALID: ${String(error)}`);
172
+ throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`);
122
173
  }
123
- if (!isArtifactRepairIntent(value))
124
- throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
174
+ if (!isArtifactRecoveryIntent(value))
175
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
125
176
  return value;
126
177
  })
127
178
  .filter((value) => value.recoveryOperationId === operationId));
128
179
  }
129
- export { readArtifactRepairIntent };
180
+ export { intentPath as artifactRecoveryIntentPath };
130
181
  //# sourceMappingURL=artifact-repair-intent.js.map
@@ -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) {
@@ -265,7 +266,7 @@ function lifecycleRecoveryIntentMatches(intent, selector) {
265
266
  return intent.taskId === selector.taskId
266
267
  && intent.family === selector.family
267
268
  && intent.artifact === selector.artifact
268
- && (intent.state === 'commit-started' || intent.state === 'consumed');
269
+ && (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
269
270
  }
270
271
  /**
271
272
  * Completes only the durable tail of a committed task event after a process
@@ -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