@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
@@ -0,0 +1,572 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import { writeDurableFile } from '../fs/durable-write.ts';
6
+ import { readStableFileSync } from './secure-fs.ts';
7
+ import { parseArtifactName } from './artifact-name.ts';
8
+ import { canonicalSemanticDigest, sha256Content } from './artifact-operations.ts';
9
+ import {
10
+ artifactRecoveryRoot,
11
+ readArtifactRecoveryIntent,
12
+ writeArtifactRecoveryIntent
13
+ } from './artifact-repair-intent.ts';
14
+ import type {
15
+ ArtifactRecoveryIntent,
16
+ ArtifactRecoveryPhase,
17
+ ArtifactRecoveryState
18
+ } from './artifact-repair-intent.ts';
19
+ import type { ArtifactSchemaFamily } from './artifact-schema.ts';
20
+ import { withTaskExecutionLock } from './task-execution-lock.ts';
21
+
22
+ const MAX_ARTIFACT_BYTES = 1024 * 1024;
23
+
24
+ export type ArtifactRecoveryTuple = Readonly<{
25
+ taskId: string;
26
+ family: ArtifactSchemaFamily;
27
+ artifact: string;
28
+ round: number;
29
+ requestId: string;
30
+ phase?: ArtifactRecoveryPhase | null;
31
+ authorityDigest?: string | null;
32
+ }>;
33
+
34
+ export type ArtifactRecoveryContext = Readonly<ArtifactRecoveryTuple & {
35
+ repoRoot: string;
36
+ taskDir: string;
37
+ recoveryId: string;
38
+ stagingId: string;
39
+ formalPath: string;
40
+ stagingPath: string;
41
+ baselinePath: string;
42
+ generationsPath: string;
43
+ baselineSha256: string;
44
+ baselineSemanticDigest: string;
45
+ }>;
46
+
47
+ export type ArtifactRecoveryOptions = Readonly<{
48
+ repoRoot: string;
49
+ taskDir: string;
50
+ recoveryId?: string;
51
+ lockAlreadyHeld?: boolean;
52
+ expectedFinalSha256?: string;
53
+ expectedFinalSemanticDigest?: string;
54
+ }>;
55
+
56
+ export type StagedArtifactCandidate = Readonly<{
57
+ recoveryId: string;
58
+ candidateSha256: string;
59
+ semanticDigest: string;
60
+ path: string;
61
+ }>;
62
+
63
+ export type ArtifactRecoveryReconcileResult = Readonly<{
64
+ status: ArtifactRecoveryState | 'indeterminate';
65
+ intent: ArtifactRecoveryIntent;
66
+ }>;
67
+
68
+ export class ArtifactRecoveryError extends Error {
69
+ readonly code: string;
70
+
71
+ constructor(code: string, message: string) {
72
+ super(`${code}: ${message}`);
73
+ this.name = 'ArtifactRecoveryError';
74
+ this.code = code;
75
+ }
76
+ }
77
+
78
+ function fail(code: string, message: string): never {
79
+ throw new ArtifactRecoveryError(code, message);
80
+ }
81
+
82
+ function runLocked<T>(
83
+ context: Pick<ArtifactRecoveryContext, 'repoRoot' | 'taskId'>,
84
+ owner: string,
85
+ callback: () => T,
86
+ lockAlreadyHeld = false
87
+ ): T {
88
+ return lockAlreadyHeld ? callback() : withTaskExecutionLock(context.repoRoot, context.taskId, owner, callback);
89
+ }
90
+
91
+ function assertTaskId(taskId: string): void {
92
+ if (!/^TASK-\d{8}-\d{6}$/u.test(taskId)) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'task id is invalid');
93
+ }
94
+
95
+ function assertDigest(value: string, name: string): void {
96
+ if (!/^[a-f0-9]{64}$/u.test(value)) fail('ARTIFACT_RECOVERY_DIGEST_INVALID', `${name} must be a lowercase SHA-256 digest`);
97
+ }
98
+
99
+ function assertRecoveryId(value: string): void {
100
+ if (!/^[a-f0-9-]{16,64}$/u.test(value)) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery id is invalid');
101
+ }
102
+
103
+ function assertTuple(tuple: ArtifactRecoveryTuple): void {
104
+ assertTaskId(tuple.taskId);
105
+ if (path.basename(tuple.artifact) !== tuple.artifact || !tuple.artifact.endsWith('.md')) {
106
+ fail('ARTIFACT_RECOVERY_TARGET_INVALID', 'artifact must be a canonical top-level Markdown file');
107
+ }
108
+ const parsed = parseArtifactName(tuple.artifact);
109
+ if (!parsed || parsed.family !== tuple.family || parsed.round !== tuple.round) {
110
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'artifact tuple does not match its canonical family and round');
111
+ }
112
+ if (!tuple.requestId || /[\r\n]/u.test(tuple.requestId)) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'request id is invalid');
113
+ if (!Number.isSafeInteger(tuple.round) || tuple.round < 1) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'round is invalid');
114
+ }
115
+
116
+ function assertDirectory(directory: string, label: string): void {
117
+ let stat: fs.Stats;
118
+ try { stat = fs.lstatSync(directory); }
119
+ catch (error) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`); }
120
+ if (!stat!.isDirectory() || stat!.isSymbolicLink()) fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a real directory`);
121
+ }
122
+
123
+ function assertRegular(file: string, label: string): fs.Stats {
124
+ let stat: fs.Stats;
125
+ try { stat = fs.lstatSync(file); }
126
+ catch (error) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`); }
127
+ if (stat!.isSymbolicLink() || !stat!.isFile()) fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a regular file`);
128
+ return stat!;
129
+ }
130
+
131
+ function ensureOwnedDirectoryTree(rootInput: string, targetInput: string): void {
132
+ const root = path.resolve(rootInput);
133
+ const target = path.resolve(targetInput);
134
+ const relative = path.relative(root, target);
135
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
136
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must remain inside the repository');
137
+ }
138
+ assertDirectory(root, 'repository root');
139
+ let current = root;
140
+ for (const segment of relative.split(path.sep)) {
141
+ current = path.join(current, segment);
142
+ let stat: fs.Stats;
143
+ try {
144
+ stat = fs.lstatSync(current);
145
+ } catch (error) {
146
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
147
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory is unavailable: ${String(error)}`);
148
+ }
149
+ try { fs.mkdirSync(current, { mode: 0o700 }); }
150
+ catch (mkdirError) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be created: ${String(mkdirError)}`); }
151
+ try { stat = fs.lstatSync(current); }
152
+ catch (lstatError) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be verified: ${String(lstatError)}`); }
153
+ }
154
+ if (stat!.isSymbolicLink() || !stat!.isDirectory()) fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory must be a real directory');
155
+ if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat!.uid !== process.getuid()) {
156
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory is not owned by the current user');
157
+ }
158
+ }
159
+ }
160
+
161
+ function assertRecoveryRoot(context: ArtifactRecoveryContext): void {
162
+ ensureOwnedDirectoryTree(context.repoRoot, path.dirname(context.stagingPath));
163
+ }
164
+
165
+ function unlinkIfPresent(file: string): void {
166
+ try {
167
+ fs.unlinkSync(file);
168
+ } catch (error) {
169
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
170
+ }
171
+ }
172
+
173
+ function writeBytes(target: string, bytes: Buffer): void {
174
+ if (bytes.length > MAX_ARTIFACT_BYTES) fail('ARTIFACT_RECOVERY_SIZE_LIMIT', 'candidate exceeds the bounded artifact size limit');
175
+ writeDurableFile(target, bytes.toString('utf8'), { mode: 0o600, replace: true });
176
+ }
177
+
178
+ function contextPaths(
179
+ tuple: ArtifactRecoveryTuple,
180
+ options: ArtifactRecoveryOptions,
181
+ recoveryId: string
182
+ ): ArtifactRecoveryContext {
183
+ const taskDir = path.resolve(options.taskDir);
184
+ const root = artifactRecoveryRoot(taskDir, recoveryId);
185
+ return {
186
+ ...tuple,
187
+ repoRoot: options.repoRoot,
188
+ taskDir,
189
+ recoveryId,
190
+ stagingId: recoveryId,
191
+ formalPath: path.join(taskDir, tuple.artifact),
192
+ stagingPath: path.join(root, 'candidate.md'),
193
+ baselinePath: path.join(root, 'baseline.md'),
194
+ generationsPath: path.join(root, 'generations'),
195
+ baselineSha256: '',
196
+ baselineSemanticDigest: ''
197
+ };
198
+ }
199
+
200
+ function readIntentForContext(context: ArtifactRecoveryContext): ArtifactRecoveryIntent {
201
+ const intent = readArtifactRecoveryIntent(context.repoRoot, context.taskId, context.family, context.artifact);
202
+ if (!intent) fail('ARTIFACT_RECOVERY_INTENT_MISSING', 'recovery journal is missing');
203
+ if (intent.recoveryOperationId !== context.recoveryId
204
+ || intent.stagingId !== context.stagingId
205
+ || intent.round !== context.round
206
+ || intent.requestId !== context.requestId) {
207
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery context does not match the journal');
208
+ }
209
+ return intent;
210
+ }
211
+
212
+ function contextWithIntent(context: ArtifactRecoveryContext, intent: ArtifactRecoveryIntent): ArtifactRecoveryContext {
213
+ return {
214
+ ...context,
215
+ phase: intent.phase,
216
+ authorityDigest: intent.authorityDigest,
217
+ baselineSha256: intent.baselineSha256,
218
+ baselineSemanticDigest: intent.baselineSemanticDigest
219
+ };
220
+ }
221
+
222
+ function createIntent(context: ArtifactRecoveryContext, now: number): ArtifactRecoveryIntent {
223
+ return {
224
+ version: 4,
225
+ taskId: context.taskId,
226
+ family: context.family,
227
+ artifact: context.artifact,
228
+ round: context.round,
229
+ state: 'awaiting-preflight-recovery',
230
+ baselineSha256: context.baselineSha256,
231
+ baselineSemanticDigest: context.baselineSemanticDigest,
232
+ stagingId: context.stagingId,
233
+ candidateSha256: context.baselineSha256,
234
+ preflightArtifactSha256: null,
235
+ preflightSemanticDigest: null,
236
+ activeGenerationSha256: null,
237
+ activeGenerationSemanticDigest: null,
238
+ pendingGenerationSha256: null,
239
+ pendingGenerationSemanticDigest: null,
240
+ finalArtifactSha256: null,
241
+ finalSemanticDigest: null,
242
+ recoveryOperationId: context.recoveryId,
243
+ phase: context.phase ?? null,
244
+ authorityDigest: context.authorityDigest ?? null,
245
+ requestId: context.requestId,
246
+ errorCode: null,
247
+ errorMessage: null,
248
+ createdAt: now,
249
+ updatedAt: now
250
+ };
251
+ }
252
+
253
+ export function beginArtifactRecovery(
254
+ tuple: ArtifactRecoveryTuple,
255
+ baselineBytes: Buffer,
256
+ options: ArtifactRecoveryOptions
257
+ ): ArtifactRecoveryContext {
258
+ assertTuple(tuple);
259
+ assertDirectory(options.taskDir, 'task directory');
260
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
261
+ assertRecoveryId(context.recoveryId);
262
+ const targetStat = assertRegular(context.formalPath, 'formal artifact');
263
+ const rootParent = path.dirname(context.stagingPath);
264
+ ensureOwnedDirectoryTree(options.repoRoot, rootParent);
265
+ const stagingStat = fs.lstatSync(rootParent);
266
+ if (stagingStat.dev !== targetStat.dev) fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must share the formal artifact file system');
267
+ const baselineSha256 = sha256Content(baselineBytes.toString('utf8'));
268
+ const prepared = { ...context, baselineSha256, baselineSemanticDigest: canonicalSemanticDigest(baselineBytes.toString('utf8')) };
269
+ return runLocked(prepared, 'task-artifact.recovery.begin', () => {
270
+ assertRecoveryRoot(prepared);
271
+ const current = readStableFileSync(prepared.formalPath, { maxBytes: MAX_ARTIFACT_BYTES }).bytes;
272
+ if (sha256Content(current.toString('utf8')) !== baselineSha256 || !current.equals(baselineBytes)) {
273
+ fail('ARTIFACT_RECOVERY_BASELINE_MISMATCH', 'formal artifact does not match the recovery baseline');
274
+ }
275
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
276
+ if (existing && !['aborted', 'consumed', 'passed'].includes(existing.state)) {
277
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
278
+ }
279
+ writeBytes(prepared.baselinePath, baselineBytes);
280
+ writeBytes(prepared.stagingPath, baselineBytes);
281
+ writeArtifactRecoveryIntent(prepared.repoRoot, createIntent(prepared, Date.now()), { expected: existing });
282
+ return prepared;
283
+ }, options.lockAlreadyHeld);
284
+ }
285
+
286
+ export function recordArtifactRecoveryPassed(
287
+ tuple: ArtifactRecoveryTuple,
288
+ options: ArtifactRecoveryOptions
289
+ ): ArtifactRecoveryContext {
290
+ assertTuple(tuple);
291
+ assertDirectory(options.taskDir, 'task directory');
292
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
293
+ assertRecoveryId(context.recoveryId);
294
+ assertRegular(context.formalPath, 'formal artifact');
295
+ return runLocked(context, 'task-artifact.recovery.record-passed', () => {
296
+ assertRecoveryRoot(context);
297
+ const current = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
298
+ const prepared = {
299
+ ...context,
300
+ baselineSha256: current.sha256,
301
+ baselineSemanticDigest: canonicalSemanticDigest(current.bytes.toString('utf8'))
302
+ };
303
+ if ((options.expectedFinalSha256 !== undefined) !== (options.expectedFinalSemanticDigest !== undefined)) {
304
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'fast-path provenance requires both finalizer digests');
305
+ }
306
+ if (options.expectedFinalSha256 !== undefined && options.expectedFinalSemanticDigest !== undefined
307
+ && (current.sha256 !== options.expectedFinalSha256 || prepared.baselineSemanticDigest !== options.expectedFinalSemanticDigest)) {
308
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'formal artifact does not match the finalizer digests');
309
+ }
310
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
311
+ if (existing && (existing.state === 'passed' || existing.state === 'consumed')
312
+ && existing.finalArtifactSha256 === prepared.baselineSha256
313
+ && existing.finalSemanticDigest === prepared.baselineSemanticDigest) {
314
+ return recoveryContextFromIntent(prepared.repoRoot, prepared.taskDir, existing);
315
+ }
316
+ if (existing && existing.state !== 'aborted' && existing.state !== 'passed') {
317
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
318
+ }
319
+ const base = createIntent(prepared, Date.now());
320
+ writeBytes(prepared.baselinePath, current.bytes);
321
+ writeBytes(prepared.stagingPath, current.bytes);
322
+ writeArtifactRecoveryIntent(prepared.repoRoot, base, { expected: existing });
323
+ const staged = stageArtifactCandidate(prepared, current.bytes, { lockAlreadyHeld: true });
324
+ prepareArtifactRecoveryCommit(prepared, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: true });
325
+ commitArtifactRecovery(prepared, { lockAlreadyHeld: true });
326
+ prepareArtifactRecoveryFinal(prepared, current.bytes, { lockAlreadyHeld: true });
327
+ commitArtifactRecovery(prepared, { lockAlreadyHeld: true });
328
+ return prepared;
329
+ }, options.lockAlreadyHeld);
330
+ }
331
+
332
+ export function stageArtifactCandidate(
333
+ context: ArtifactRecoveryContext,
334
+ candidateBytes: Buffer,
335
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
336
+ ): StagedArtifactCandidate {
337
+ return runLocked(context, 'task-artifact.recovery.stage', () => {
338
+ assertRecoveryRoot(context);
339
+ const intent = readIntentForContext(context);
340
+ if (intent.state !== 'awaiting-preflight-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot stage a candidate from '${intent.state}'`);
341
+ const candidateSha256 = sha256Content(candidateBytes.toString('utf8'));
342
+ const semanticDigest = canonicalSemanticDigest(candidateBytes.toString('utf8'));
343
+ writeBytes(context.stagingPath, candidateBytes);
344
+ const next = { ...intent, candidateSha256, updatedAt: Date.now() };
345
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
346
+ return { recoveryId: context.recoveryId, candidateSha256, semanticDigest, path: context.stagingPath };
347
+ }, options.lockAlreadyHeld);
348
+ }
349
+
350
+ function generationPath(context: ArtifactRecoveryContext, sha256: string): string {
351
+ assertDigest(sha256, 'generation digest');
352
+ return path.join(context.generationsPath, `${sha256}.md`);
353
+ }
354
+
355
+ /** Persist and verify an immutable digest-addressed snapshot before it is journaled. */
356
+ function writeGeneration(context: ArtifactRecoveryContext, bytes: Buffer, sha256: string, semanticDigest: string): void {
357
+ ensureOwnedDirectoryTree(context.repoRoot, context.generationsPath);
358
+ const target = generationPath(context, sha256);
359
+ if (fs.existsSync(target)) {
360
+ const existing = readStableFileSync(target, { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
361
+ if (canonicalSemanticDigest(existing.bytes.toString('utf8')) !== semanticDigest) {
362
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'existing generation does not match its semantic digest');
363
+ }
364
+ return;
365
+ }
366
+ writeBytes(target, bytes);
367
+ fs.chmodSync(target, 0o400);
368
+ const sealed = readStableFileSync(target, { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
369
+ if (canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== semanticDigest) {
370
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'generation does not match its validated semantic digest');
371
+ }
372
+ }
373
+
374
+ function publishGeneration(context: ArtifactRecoveryContext, sha256: string, semanticDigest: string, baselineSha256: string): void {
375
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
376
+ const generation = readStableFileSync(generationPath(context, sha256), { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
377
+ if (canonicalSemanticDigest(generation.bytes.toString('utf8')) !== semanticDigest) {
378
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'generation semantic digest does not match the journal');
379
+ }
380
+ if (target.sha256 === sha256) return;
381
+ if (target.sha256 !== baselineSha256) fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact changed outside the expected generation');
382
+ const publishPath = path.join(path.dirname(context.stagingPath), 'publish.md');
383
+ unlinkIfPresent(publishPath);
384
+ writeBytes(publishPath, generation.bytes);
385
+ fs.chmodSync(publishPath, 0o400);
386
+ fs.renameSync(publishPath, context.formalPath);
387
+ const published = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
388
+ if (canonicalSemanticDigest(published.bytes.toString('utf8')) !== semanticDigest) {
389
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'published artifact does not match its generation');
390
+ }
391
+ }
392
+
393
+ export function prepareArtifactRecoveryCommit(
394
+ context: ArtifactRecoveryContext,
395
+ finalSha256: string,
396
+ finalSemanticDigest: string,
397
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
398
+ ): ArtifactRecoveryIntent {
399
+ assertDigest(finalSha256, 'final artifact digest');
400
+ assertDigest(finalSemanticDigest, 'final semantic digest');
401
+ return runLocked(context, 'task-artifact.recovery.prepare', () => {
402
+ const intent = readIntentForContext(context);
403
+ if (intent.state !== 'awaiting-preflight-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a preflight from '${intent.state}'`);
404
+ assertRecoveryRoot(context);
405
+ const staged = readStableFileSync(context.stagingPath, { maxBytes: MAX_ARTIFACT_BYTES });
406
+ if (staged.sha256 !== finalSha256 || canonicalSemanticDigest(staged.bytes.toString('utf8')) !== finalSemanticDigest) {
407
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'staged candidate does not match the finalizer digest');
408
+ }
409
+ writeGeneration(context, staged.bytes, finalSha256, finalSemanticDigest);
410
+ const next: ArtifactRecoveryIntent = {
411
+ ...intent,
412
+ state: 'preflight-ready',
413
+ candidateSha256: staged.sha256,
414
+ preflightArtifactSha256: finalSha256,
415
+ preflightSemanticDigest: finalSemanticDigest,
416
+ activeGenerationSha256: finalSha256,
417
+ activeGenerationSemanticDigest: finalSemanticDigest,
418
+ errorCode: null,
419
+ errorMessage: null,
420
+ updatedAt: Date.now()
421
+ };
422
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
423
+ return next;
424
+ }, options.lockAlreadyHeld);
425
+ }
426
+
427
+ function markCommitStarted(context: ArtifactRecoveryContext, intent: ArtifactRecoveryIntent): ArtifactRecoveryIntent {
428
+ if (intent.state === 'preflight-commit-started' || intent.state === 'commit-started') return intent;
429
+ if (intent.state !== 'preflight-ready' && intent.state !== 'full-finalizer-ready') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot commit from '${intent.state}'`);
430
+ const next: ArtifactRecoveryIntent = { ...intent, state: intent.state === 'preflight-ready' ? 'preflight-commit-started' : 'commit-started', updatedAt: Date.now() };
431
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
432
+ return next;
433
+ }
434
+
435
+ export function commitArtifactRecovery(
436
+ context: ArtifactRecoveryContext,
437
+ options: Readonly<{ lockAlreadyHeld?: boolean; afterPublish?: () => void }> = {}
438
+ ): ArtifactRecoveryIntent {
439
+ return runLocked(context, 'task-artifact.recovery.commit', () => {
440
+ let intent = readIntentForContext(context);
441
+ if (intent.state === 'passed' || intent.state === 'consumed') return intent;
442
+ assertRecoveryRoot(context);
443
+ intent = markCommitStarted(context, intent);
444
+ if (intent.state === 'preflight-commit-started') {
445
+ publishGeneration(context, intent.activeGenerationSha256!, intent.activeGenerationSemanticDigest!, intent.baselineSha256);
446
+ const passed = { ...intent, state: 'preflight-passed' as const, updatedAt: Date.now() };
447
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
448
+ return passed;
449
+ }
450
+ publishGeneration(context, intent.pendingGenerationSha256!, intent.pendingGenerationSemanticDigest!, intent.activeGenerationSha256!);
451
+ options.afterPublish?.();
452
+ const passed: ArtifactRecoveryIntent = {
453
+ ...intent, state: 'passed', activeGenerationSha256: intent.pendingGenerationSha256,
454
+ activeGenerationSemanticDigest: intent.pendingGenerationSemanticDigest,
455
+ pendingGenerationSha256: null, pendingGenerationSemanticDigest: null, updatedAt: Date.now()
456
+ };
457
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
458
+ return passed;
459
+ }, options.lockAlreadyHeld);
460
+ }
461
+
462
+ export function prepareArtifactRecoveryFinal(
463
+ context: ArtifactRecoveryContext,
464
+ finalBytes: Buffer,
465
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
466
+ ): ArtifactRecoveryIntent {
467
+ return runLocked(context, 'task-artifact.recovery.finalize', () => {
468
+ const intent = readIntentForContext(context);
469
+ if (intent.state !== 'preflight-passed') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot finalize from '${intent.state}'`);
470
+ const finalSha256 = sha256Content(finalBytes.toString('utf8'));
471
+ const finalSemanticDigest = canonicalSemanticDigest(finalBytes.toString('utf8'));
472
+ if (finalSha256 !== intent.activeGenerationSha256) writeGeneration(context, finalBytes, finalSha256, finalSemanticDigest);
473
+ const next: ArtifactRecoveryIntent = {
474
+ ...intent, state: 'full-finalizer-ready', pendingGenerationSha256: finalSha256,
475
+ pendingGenerationSemanticDigest: finalSemanticDigest, finalArtifactSha256: finalSha256,
476
+ finalSemanticDigest, updatedAt: Date.now()
477
+ };
478
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
479
+ return next;
480
+ }, options.lockAlreadyHeld);
481
+ }
482
+
483
+ export function abortArtifactRecovery(
484
+ context: ArtifactRecoveryContext,
485
+ code: string,
486
+ message: string,
487
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
488
+ ): ArtifactRecoveryIntent {
489
+ return runLocked(context, 'task-artifact.recovery.abort', () => {
490
+ const intent = readIntentForContext(context);
491
+ assertRegular(context.baselinePath, 'baseline backup');
492
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
493
+ if (target.sha256 !== intent.baselineSha256) fs.renameSync(context.baselinePath, context.formalPath);
494
+ const restored = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
495
+ if (restored.sha256 !== intent.baselineSha256) fail('ARTIFACT_RECOVERY_INDETERMINATE', 'baseline restore could not be verified');
496
+ const aborted: ArtifactRecoveryIntent = {
497
+ ...intent,
498
+ state: 'aborted',
499
+ errorCode: code,
500
+ errorMessage: message,
501
+ updatedAt: Date.now()
502
+ };
503
+ writeArtifactRecoveryIntent(context.repoRoot, aborted, { expected: intent });
504
+ return aborted;
505
+ }, options.lockAlreadyHeld);
506
+ }
507
+
508
+ export function reconcileArtifactRecovery(
509
+ context: ArtifactRecoveryContext,
510
+ options: Readonly<{
511
+ lockAlreadyHeld?: boolean;
512
+ validateFinal?: () => boolean;
513
+ restoreBaseline?: boolean;
514
+ afterPublish?: () => void;
515
+ }> = {}
516
+ ): ArtifactRecoveryReconcileResult {
517
+ return runLocked(context, 'task-artifact.recovery.reconcile', () => {
518
+ const intent = readIntentForContext(context);
519
+ if (intent.state !== 'preflight-commit-started' && intent.state !== 'commit-started') return { status: intent.state, intent };
520
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
521
+ const expected = intent.state === 'preflight-commit-started' ? intent.activeGenerationSha256! : intent.pendingGenerationSha256!;
522
+ if (target.sha256 === expected && (options.validateFinal?.() ?? true)) {
523
+ const next = intent.state === 'preflight-commit-started'
524
+ ? { ...intent, state: 'preflight-passed' as const, updatedAt: Date.now() }
525
+ : { ...intent, state: 'passed' as const, activeGenerationSha256: intent.pendingGenerationSha256, activeGenerationSemanticDigest: intent.pendingGenerationSemanticDigest, pendingGenerationSha256: null, pendingGenerationSemanticDigest: null, updatedAt: Date.now() };
526
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
527
+ return { status: next.state, intent: next };
528
+ }
529
+ if (target.sha256 === (intent.state === 'preflight-commit-started' ? intent.baselineSha256 : intent.activeGenerationSha256)) {
530
+ const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true, afterPublish: options.afterPublish });
531
+ return { status: retried.state, intent: retried };
532
+ }
533
+ if (options.restoreBaseline) {
534
+ const aborted = abortArtifactRecovery(context, 'ARTIFACT_RECOVERY_CONTEXT_INVALID', 'formal target is not a verifiable baseline or final candidate', { lockAlreadyHeld: true });
535
+ return { status: 'aborted', intent: aborted };
536
+ }
537
+ return { status: 'indeterminate', intent };
538
+ }, options.lockAlreadyHeld);
539
+ }
540
+
541
+ export function consumeArtifactRecovery(
542
+ context: ArtifactRecoveryContext,
543
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
544
+ ): ArtifactRecoveryIntent {
545
+ return runLocked(context, 'task-artifact.recovery.consume', () => {
546
+ const intent = readIntentForContext(context);
547
+ if (intent.state === 'consumed') return intent;
548
+ if (intent.state !== 'passed') fail('ARTIFACT_RECOVERY_STATE_INVALID', `only passed recovery can be consumed, got '${intent.state}'`);
549
+ const consumed: ArtifactRecoveryIntent = { ...intent, state: 'consumed', updatedAt: Date.now() };
550
+ writeArtifactRecoveryIntent(context.repoRoot, consumed, { expected: intent });
551
+ return consumed;
552
+ }, options.lockAlreadyHeld);
553
+ }
554
+
555
+ export function recoveryContextFromIntent(
556
+ repoRoot: string,
557
+ taskDir: string,
558
+ intent: ArtifactRecoveryIntent
559
+ ): ArtifactRecoveryContext {
560
+ const context = contextPaths({
561
+ taskId: intent.taskId,
562
+ family: intent.family,
563
+ artifact: intent.artifact,
564
+ round: intent.round,
565
+ requestId: intent.requestId,
566
+ phase: intent.phase,
567
+ authorityDigest: intent.authorityDigest
568
+ }, { repoRoot, taskDir, recoveryId: intent.recoveryOperationId }, intent.recoveryOperationId);
569
+ return contextWithIntent(context, intent);
570
+ }
571
+
572
+ export { canonicalSemanticDigest, readArtifactRecoveryIntent, sha256Content };