@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
@@ -0,0 +1,430 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { writeDurableFile } from '../fs/durable-write.js';
5
+ import { readStableFileSync } from '../host-control/secure-fs.js';
6
+ import { parseArtifactName } from './artifact-name.js';
7
+ import { canonicalSemanticDigest, sha256Content } from './artifact-operations.js';
8
+ import { artifactRecoveryRoot, readArtifactRecoveryIntent, writeArtifactRecoveryIntent } from './artifact-repair-intent.js';
9
+ import { withTaskExecutionLock } from './task-execution-lock.js';
10
+ const MAX_ARTIFACT_BYTES = 1024 * 1024;
11
+ export class ArtifactRecoveryError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(`${code}: ${message}`);
15
+ this.name = 'ArtifactRecoveryError';
16
+ this.code = code;
17
+ }
18
+ }
19
+ function fail(code, message) {
20
+ throw new ArtifactRecoveryError(code, message);
21
+ }
22
+ function runLocked(context, owner, callback, lockAlreadyHeld = false) {
23
+ return lockAlreadyHeld ? callback() : withTaskExecutionLock(context.repoRoot, context.taskId, owner, callback);
24
+ }
25
+ function assertTaskId(taskId) {
26
+ if (!/^TASK-\d{8}-\d{6}$/u.test(taskId))
27
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'task id is invalid');
28
+ }
29
+ function assertDigest(value, name) {
30
+ if (!/^[a-f0-9]{64}$/u.test(value))
31
+ fail('ARTIFACT_RECOVERY_DIGEST_INVALID', `${name} must be a lowercase SHA-256 digest`);
32
+ }
33
+ function assertRecoveryId(value) {
34
+ if (!/^[a-f0-9-]{16,64}$/u.test(value))
35
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery id is invalid');
36
+ }
37
+ function assertTuple(tuple) {
38
+ assertTaskId(tuple.taskId);
39
+ if (path.basename(tuple.artifact) !== tuple.artifact || !tuple.artifact.endsWith('.md')) {
40
+ fail('ARTIFACT_RECOVERY_TARGET_INVALID', 'artifact must be a canonical top-level Markdown file');
41
+ }
42
+ const parsed = parseArtifactName(tuple.artifact);
43
+ if (!parsed || parsed.family !== tuple.family || parsed.round !== tuple.round) {
44
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'artifact tuple does not match its canonical family and round');
45
+ }
46
+ if (!tuple.requestId || /[\r\n]/u.test(tuple.requestId))
47
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'request id is invalid');
48
+ if (!Number.isSafeInteger(tuple.round) || tuple.round < 1)
49
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'round is invalid');
50
+ }
51
+ function assertDirectory(directory, label) {
52
+ let stat;
53
+ try {
54
+ stat = fs.lstatSync(directory);
55
+ }
56
+ catch (error) {
57
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`);
58
+ }
59
+ if (!stat.isDirectory() || stat.isSymbolicLink())
60
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a real directory`);
61
+ }
62
+ function assertRegular(file, label) {
63
+ let stat;
64
+ try {
65
+ stat = fs.lstatSync(file);
66
+ }
67
+ catch (error) {
68
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`);
69
+ }
70
+ if (stat.isSymbolicLink() || !stat.isFile())
71
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a regular file`);
72
+ return stat;
73
+ }
74
+ function ensureOwnedDirectoryTree(rootInput, targetInput) {
75
+ const root = path.resolve(rootInput);
76
+ const target = path.resolve(targetInput);
77
+ const relative = path.relative(root, target);
78
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
79
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must remain inside the repository');
80
+ }
81
+ assertDirectory(root, 'repository root');
82
+ let current = root;
83
+ for (const segment of relative.split(path.sep)) {
84
+ current = path.join(current, segment);
85
+ let stat;
86
+ try {
87
+ stat = fs.lstatSync(current);
88
+ }
89
+ catch (error) {
90
+ if (error.code !== 'ENOENT') {
91
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory is unavailable: ${String(error)}`);
92
+ }
93
+ try {
94
+ fs.mkdirSync(current, { mode: 0o700 });
95
+ }
96
+ catch (mkdirError) {
97
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be created: ${String(mkdirError)}`);
98
+ }
99
+ try {
100
+ stat = fs.lstatSync(current);
101
+ }
102
+ catch (lstatError) {
103
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be verified: ${String(lstatError)}`);
104
+ }
105
+ }
106
+ if (stat.isSymbolicLink() || !stat.isDirectory())
107
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory must be a real directory');
108
+ if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
109
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory is not owned by the current user');
110
+ }
111
+ }
112
+ }
113
+ function assertRecoveryRoot(context) {
114
+ ensureOwnedDirectoryTree(context.repoRoot, path.dirname(context.stagingPath));
115
+ }
116
+ function unlinkIfPresent(file) {
117
+ try {
118
+ fs.unlinkSync(file);
119
+ }
120
+ catch (error) {
121
+ if (error.code !== 'ENOENT')
122
+ throw error;
123
+ }
124
+ }
125
+ function writeBytes(target, bytes) {
126
+ if (bytes.length > MAX_ARTIFACT_BYTES)
127
+ fail('ARTIFACT_RECOVERY_SIZE_LIMIT', 'candidate exceeds the bounded artifact size limit');
128
+ writeDurableFile(target, bytes.toString('utf8'), { mode: 0o600, replace: true });
129
+ }
130
+ function contextPaths(tuple, options, recoveryId) {
131
+ const taskDir = path.resolve(options.taskDir);
132
+ const root = artifactRecoveryRoot(taskDir, recoveryId);
133
+ return {
134
+ ...tuple,
135
+ repoRoot: options.repoRoot,
136
+ taskDir,
137
+ recoveryId,
138
+ stagingId: recoveryId,
139
+ formalPath: path.join(taskDir, tuple.artifact),
140
+ stagingPath: path.join(root, 'candidate.md'),
141
+ baselinePath: path.join(root, 'baseline.md'),
142
+ finalPath: path.join(root, 'final.md'),
143
+ baselineSha256: '',
144
+ baselineSemanticDigest: ''
145
+ };
146
+ }
147
+ function readIntentForContext(context) {
148
+ const intent = readArtifactRecoveryIntent(context.repoRoot, context.taskId, context.family, context.artifact);
149
+ if (!intent)
150
+ fail('ARTIFACT_RECOVERY_INTENT_MISSING', 'recovery journal is missing');
151
+ if (intent.recoveryOperationId !== context.recoveryId
152
+ || intent.stagingId !== context.stagingId
153
+ || intent.round !== context.round
154
+ || intent.requestId !== context.requestId) {
155
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery context does not match the journal');
156
+ }
157
+ return intent;
158
+ }
159
+ function contextWithIntent(context, intent) {
160
+ return {
161
+ ...context,
162
+ phase: intent.phase,
163
+ authorityDigest: intent.authorityDigest,
164
+ baselineSha256: intent.baselineSha256,
165
+ baselineSemanticDigest: intent.baselineSemanticDigest
166
+ };
167
+ }
168
+ function createIntent(context, now) {
169
+ return {
170
+ version: 3,
171
+ taskId: context.taskId,
172
+ family: context.family,
173
+ artifact: context.artifact,
174
+ round: context.round,
175
+ state: 'awaiting-recovery',
176
+ baselineSha256: context.baselineSha256,
177
+ baselineSemanticDigest: context.baselineSemanticDigest,
178
+ stagingId: context.stagingId,
179
+ candidateSha256: context.baselineSha256,
180
+ finalArtifactSha256: null,
181
+ finalSemanticDigest: null,
182
+ recoveryOperationId: context.recoveryId,
183
+ phase: context.phase ?? null,
184
+ authorityDigest: context.authorityDigest ?? null,
185
+ requestId: context.requestId,
186
+ errorCode: null,
187
+ errorMessage: null,
188
+ createdAt: now,
189
+ updatedAt: now
190
+ };
191
+ }
192
+ export function beginArtifactRecovery(tuple, baselineBytes, options) {
193
+ assertTuple(tuple);
194
+ assertDirectory(options.taskDir, 'task directory');
195
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
196
+ assertRecoveryId(context.recoveryId);
197
+ const targetStat = assertRegular(context.formalPath, 'formal artifact');
198
+ const rootParent = path.dirname(context.stagingPath);
199
+ ensureOwnedDirectoryTree(options.repoRoot, rootParent);
200
+ const stagingStat = fs.lstatSync(rootParent);
201
+ if (stagingStat.dev !== targetStat.dev)
202
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must share the formal artifact file system');
203
+ const baselineSha256 = sha256Content(baselineBytes.toString('utf8'));
204
+ const prepared = { ...context, baselineSha256, baselineSemanticDigest: canonicalSemanticDigest(baselineBytes.toString('utf8')) };
205
+ return runLocked(prepared, 'task-artifact.recovery.begin', () => {
206
+ assertRecoveryRoot(prepared);
207
+ const current = readStableFileSync(prepared.formalPath, { maxBytes: MAX_ARTIFACT_BYTES }).bytes;
208
+ if (sha256Content(current.toString('utf8')) !== baselineSha256 || !current.equals(baselineBytes)) {
209
+ fail('ARTIFACT_RECOVERY_BASELINE_MISMATCH', 'formal artifact does not match the recovery baseline');
210
+ }
211
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
212
+ if (existing && existing.state !== 'aborted' && existing.state !== 'consumed') {
213
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
214
+ }
215
+ writeBytes(prepared.baselinePath, baselineBytes);
216
+ writeBytes(prepared.stagingPath, baselineBytes);
217
+ writeArtifactRecoveryIntent(prepared.repoRoot, createIntent(prepared, Date.now()), { expected: existing });
218
+ return prepared;
219
+ }, options.lockAlreadyHeld);
220
+ }
221
+ export function recordArtifactRecoveryPassed(tuple, options) {
222
+ assertTuple(tuple);
223
+ assertDirectory(options.taskDir, 'task directory');
224
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
225
+ assertRecoveryId(context.recoveryId);
226
+ assertRegular(context.formalPath, 'formal artifact');
227
+ return runLocked(context, 'task-artifact.recovery.record-passed', () => {
228
+ const current = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
229
+ const prepared = {
230
+ ...context,
231
+ baselineSha256: current.sha256,
232
+ baselineSemanticDigest: canonicalSemanticDigest(current.bytes.toString('utf8'))
233
+ };
234
+ if ((options.expectedFinalSha256 !== undefined) !== (options.expectedFinalSemanticDigest !== undefined)) {
235
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'fast-path provenance requires both finalizer digests');
236
+ }
237
+ if (options.expectedFinalSha256 !== undefined && options.expectedFinalSemanticDigest !== undefined
238
+ && (current.sha256 !== options.expectedFinalSha256 || prepared.baselineSemanticDigest !== options.expectedFinalSemanticDigest)) {
239
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'formal artifact does not match the finalizer digests');
240
+ }
241
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
242
+ if (existing && (existing.state === 'passed' || existing.state === 'consumed')
243
+ && existing.finalArtifactSha256 === prepared.baselineSha256
244
+ && existing.finalSemanticDigest === prepared.baselineSemanticDigest) {
245
+ return recoveryContextFromIntent(prepared.repoRoot, prepared.taskDir, existing);
246
+ }
247
+ if (existing && existing.state !== 'aborted') {
248
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
249
+ }
250
+ const now = Date.now();
251
+ const next = {
252
+ ...createIntent(prepared, now),
253
+ state: 'passed',
254
+ candidateSha256: prepared.baselineSha256,
255
+ finalArtifactSha256: prepared.baselineSha256,
256
+ finalSemanticDigest: prepared.baselineSemanticDigest
257
+ };
258
+ writeArtifactRecoveryIntent(prepared.repoRoot, next, { expected: existing });
259
+ return prepared;
260
+ }, options.lockAlreadyHeld);
261
+ }
262
+ export function stageArtifactCandidate(context, candidateBytes, options = {}) {
263
+ return runLocked(context, 'task-artifact.recovery.stage', () => {
264
+ assertRecoveryRoot(context);
265
+ const intent = readIntentForContext(context);
266
+ if (intent.state !== 'awaiting-recovery')
267
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot stage a candidate from '${intent.state}'`);
268
+ const candidateSha256 = sha256Content(candidateBytes.toString('utf8'));
269
+ const semanticDigest = canonicalSemanticDigest(candidateBytes.toString('utf8'));
270
+ writeBytes(context.stagingPath, candidateBytes);
271
+ const next = { ...intent, candidateSha256, updatedAt: Date.now() };
272
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
273
+ return { recoveryId: context.recoveryId, candidateSha256, semanticDigest, path: context.stagingPath };
274
+ }, options.lockAlreadyHeld);
275
+ }
276
+ export function prepareArtifactRecoveryCommit(context, finalSha256, finalSemanticDigest, options = {}) {
277
+ assertDigest(finalSha256, 'final artifact digest');
278
+ assertDigest(finalSemanticDigest, 'final semantic digest');
279
+ return runLocked(context, 'task-artifact.recovery.prepare', () => {
280
+ const intent = readIntentForContext(context);
281
+ if (intent.state !== 'awaiting-recovery')
282
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a commit from '${intent.state}'`);
283
+ assertRecoveryRoot(context);
284
+ const staged = readStableFileSync(context.stagingPath, { maxBytes: MAX_ARTIFACT_BYTES });
285
+ if (staged.sha256 !== finalSha256 || canonicalSemanticDigest(staged.bytes.toString('utf8')) !== finalSemanticDigest) {
286
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'staged candidate does not match the finalizer digest');
287
+ }
288
+ writeBytes(context.finalPath, staged.bytes);
289
+ fs.chmodSync(context.finalPath, 0o400);
290
+ const sealed = readStableFileSync(context.finalPath, { maxBytes: MAX_ARTIFACT_BYTES });
291
+ if (sealed.sha256 !== finalSha256 || canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== finalSemanticDigest) {
292
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'sealed candidate does not match the finalizer digest');
293
+ }
294
+ const next = {
295
+ ...intent,
296
+ state: 'finalize-ready',
297
+ candidateSha256: staged.sha256,
298
+ finalArtifactSha256: finalSha256,
299
+ finalSemanticDigest,
300
+ errorCode: null,
301
+ errorMessage: null,
302
+ updatedAt: Date.now()
303
+ };
304
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
305
+ return next;
306
+ }, options.lockAlreadyHeld);
307
+ }
308
+ function markCommitStarted(context, intent) {
309
+ if (intent.state === 'commit-started')
310
+ return intent;
311
+ if (intent.state !== 'finalize-ready')
312
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot commit from '${intent.state}'`);
313
+ const next = { ...intent, state: 'commit-started', updatedAt: Date.now() };
314
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
315
+ return next;
316
+ }
317
+ export function commitArtifactRecovery(context, options = {}) {
318
+ return runLocked(context, 'task-artifact.recovery.commit', () => {
319
+ let intent = readIntentForContext(context);
320
+ if (intent.state === 'passed' || intent.state === 'consumed')
321
+ return intent;
322
+ assertRecoveryRoot(context);
323
+ intent = markCommitStarted(context, intent);
324
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
325
+ if (target.sha256 === intent.finalArtifactSha256) {
326
+ const passed = { ...intent, state: 'passed', updatedAt: Date.now() };
327
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
328
+ return passed;
329
+ }
330
+ if (target.sha256 !== intent.baselineSha256)
331
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact changed outside the recovery baseline');
332
+ const sealed = readStableFileSync(context.finalPath, {
333
+ maxBytes: MAX_ARTIFACT_BYTES,
334
+ expectedSha256: intent.finalArtifactSha256
335
+ });
336
+ if (canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== intent.finalSemanticDigest) {
337
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'sealed artifact does not match the staged final semantic digest');
338
+ }
339
+ // Publish the bytes that were validated above. The sealed path is only a
340
+ // recovery input; renaming it directly would reopen a same-uid path swap
341
+ // between validation and publication.
342
+ const publishPath = path.join(path.dirname(context.finalPath), 'publish.md');
343
+ unlinkIfPresent(publishPath);
344
+ writeBytes(publishPath, sealed.bytes);
345
+ fs.chmodSync(publishPath, 0o400);
346
+ const publish = readStableFileSync(publishPath, {
347
+ maxBytes: MAX_ARTIFACT_BYTES,
348
+ expectedSha256: intent.finalArtifactSha256
349
+ });
350
+ if (canonicalSemanticDigest(publish.bytes.toString('utf8')) !== intent.finalSemanticDigest) {
351
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'publish artifact does not match the staged final semantic digest');
352
+ }
353
+ fs.renameSync(publishPath, context.formalPath);
354
+ const published = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
355
+ if (published.sha256 !== intent.finalArtifactSha256)
356
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact does not match the staged final digest');
357
+ const passed = { ...intent, state: 'passed', updatedAt: Date.now() };
358
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
359
+ return passed;
360
+ }, options.lockAlreadyHeld);
361
+ }
362
+ export function abortArtifactRecovery(context, code, message, options = {}) {
363
+ return runLocked(context, 'task-artifact.recovery.abort', () => {
364
+ const intent = readIntentForContext(context);
365
+ assertRegular(context.baselinePath, 'baseline backup');
366
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
367
+ if (target.sha256 !== intent.baselineSha256)
368
+ fs.renameSync(context.baselinePath, context.formalPath);
369
+ const restored = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
370
+ if (restored.sha256 !== intent.baselineSha256)
371
+ fail('ARTIFACT_RECOVERY_INDETERMINATE', 'baseline restore could not be verified');
372
+ const aborted = {
373
+ ...intent,
374
+ state: 'aborted',
375
+ errorCode: code,
376
+ errorMessage: message,
377
+ updatedAt: Date.now()
378
+ };
379
+ writeArtifactRecoveryIntent(context.repoRoot, aborted, { expected: intent });
380
+ return aborted;
381
+ }, options.lockAlreadyHeld);
382
+ }
383
+ export function reconcileArtifactRecovery(context, options = {}) {
384
+ return runLocked(context, 'task-artifact.recovery.reconcile', () => {
385
+ const intent = readIntentForContext(context);
386
+ if (intent.state !== 'commit-started')
387
+ return { status: intent.state, intent };
388
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
389
+ if (target.sha256 === intent.finalArtifactSha256 && (options.validateFinal?.() ?? true)) {
390
+ const passed = { ...intent, state: 'passed', updatedAt: Date.now() };
391
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
392
+ return { status: 'passed', intent: passed };
393
+ }
394
+ if (target.sha256 === intent.baselineSha256) {
395
+ const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true });
396
+ return { status: retried.state, intent: retried };
397
+ }
398
+ if (options.restoreBaseline) {
399
+ const aborted = abortArtifactRecovery(context, 'ARTIFACT_RECOVERY_CONTEXT_INVALID', 'formal target is not a verifiable baseline or final candidate', { lockAlreadyHeld: true });
400
+ return { status: 'aborted', intent: aborted };
401
+ }
402
+ return { status: 'indeterminate', intent };
403
+ }, options.lockAlreadyHeld);
404
+ }
405
+ export function consumeArtifactRecovery(context, options = {}) {
406
+ return runLocked(context, 'task-artifact.recovery.consume', () => {
407
+ const intent = readIntentForContext(context);
408
+ if (intent.state === 'consumed')
409
+ return intent;
410
+ if (intent.state !== 'passed')
411
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `only passed recovery can be consumed, got '${intent.state}'`);
412
+ const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
413
+ writeArtifactRecoveryIntent(context.repoRoot, consumed, { expected: intent });
414
+ return consumed;
415
+ }, options.lockAlreadyHeld);
416
+ }
417
+ export function recoveryContextFromIntent(repoRoot, taskDir, intent) {
418
+ const context = contextPaths({
419
+ taskId: intent.taskId,
420
+ family: intent.family,
421
+ artifact: intent.artifact,
422
+ round: intent.round,
423
+ requestId: intent.requestId,
424
+ phase: intent.phase,
425
+ authorityDigest: intent.authorityDigest
426
+ }, { repoRoot, taskDir, recoveryId: intent.recoveryOperationId }, intent.recoveryOperationId);
427
+ return contextWithIntent(context, intent);
428
+ }
429
+ export { canonicalSemanticDigest, readArtifactRecoveryIntent, sha256Content };
430
+ //# sourceMappingURL=artifact-recovery.js.map
@@ -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,65 @@ 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
+ 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
41
+ 'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
42
+ 'phase', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
43
+ 'version'
27
44
  ])
28
- && intent.version === 2
45
+ && intent.version === 3
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-recovery', 'finalize-ready', 'commit-started', 'passed', '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.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
57
+ && (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
58
+ && validRecoveryId(intent.recoveryOperationId)
59
+ && intent.stagingId === intent.recoveryOperationId
38
60
  && (intent.phase === null || ['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(String(intent.phase)))
39
61
  && (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);
62
+ && validToken(intent.requestId)
63
+ && (intent.errorCode === null || validToken(intent.errorCode))
64
+ && (intent.errorMessage === null || validToken(intent.errorMessage))
65
+ && Number.isSafeInteger(intent.createdAt)
66
+ && Number.isSafeInteger(intent.updatedAt)
67
+ && (intent.state === 'awaiting-recovery'
68
+ ? intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
69
+ : true)
70
+ && (intent.state === 'finalize-ready' || intent.state === 'commit-started' || intent.state === 'passed' || intent.state === 'consumed'
71
+ ? validDigest(intent.candidateSha256) && validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
72
+ : true)
73
+ && (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
43
74
  }
44
- function readArtifactRepairIntent(repoRoot, taskId, family, artifact) {
75
+ export function readArtifactRecoveryIntent(repoRoot, taskId, family, artifact) {
76
+ if (!validTaskId(taskId) || !validArtifact(artifact, family, parseArtifactName(artifact)?.round)) {
77
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal identity is invalid');
78
+ }
45
79
  const target = intentPath(repoRoot, taskId, family, artifact);
46
80
  if (!fs.existsSync(target))
47
81
  return null;
@@ -50,19 +84,20 @@ function readArtifactRepairIntent(repoRoot, taskId, family, artifact) {
50
84
  value = JSON.parse(fs.readFileSync(target, 'utf8'));
51
85
  }
52
86
  catch (error) {
53
- throw new Error(`ARTIFACT_REPAIR_INTENT_INVALID: ${String(error)}`);
87
+ throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`);
54
88
  }
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');
89
+ if (!isArtifactRecoveryIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
90
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
57
91
  }
58
92
  return value;
59
93
  }
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);
94
+ export function writeArtifactRecoveryIntent(repoRoot, value, options = {}) {
95
+ if (!isArtifactRecoveryIntent(value))
96
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
97
+ const intent = value;
98
+ const directory = artifactRecoveryIntentRoot(repoRoot);
64
99
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
65
- const target = intentPath(repoRoot, value.taskId, value.family, value.artifact);
100
+ const target = intentPath(repoRoot, intent.taskId, intent.family, intent.artifact);
66
101
  const lock = `${target}.lock`;
67
102
  let descriptor;
68
103
  try {
@@ -70,19 +105,19 @@ export function writeArtifactRepairIntent(repoRoot, value, options = {}) {
70
105
  }
71
106
  catch (error) {
72
107
  if (error.code === 'EEXIST') {
73
- throw new Error('ARTIFACT_REPAIR_INTENT_CONFLICT: another provenance writer owns the intent');
108
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: another journal writer owns the intent');
74
109
  }
75
110
  throw error;
76
111
  }
77
112
  try {
78
113
  const current = fs.existsSync(target)
79
- ? readArtifactRepairIntent(repoRoot, value.taskId, value.family, value.artifact)
114
+ ? readArtifactRecoveryIntent(repoRoot, intent.taskId, intent.family, intent.artifact)
80
115
  : null;
81
116
  if (!isDeepStrictEqual(current, options.expected ?? null)) {
82
- throw new Error('ARTIFACT_REPAIR_INTENT_CONFLICT: repair provenance changed during finalization');
117
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: recovery journal changed during finalization');
83
118
  }
84
119
  const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
85
- fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
120
+ fs.writeFileSync(temporary, `${JSON.stringify(intent)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
86
121
  try {
87
122
  fs.renameSync(temporary, target);
88
123
  }
@@ -90,7 +125,7 @@ export function writeArtifactRepairIntent(repoRoot, value, options = {}) {
90
125
  try {
91
126
  fs.unlinkSync(temporary);
92
127
  }
93
- catch { /* preserve primary error */ }
128
+ catch { /* preserve the primary error */ }
94
129
  throw error;
95
130
  }
96
131
  }
@@ -102,11 +137,10 @@ export function writeArtifactRepairIntent(repoRoot, value, options = {}) {
102
137
  catch { /* preserve the primary result */ }
103
138
  }
104
139
  }
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);
140
+ export function findArtifactRecoveryIntentsByOperation(repoRoot, operationId) {
141
+ if (!validToken(operationId))
142
+ throw new Error('ARTIFACT_RECOVERY_OPERATION_INVALID: recovery operation id is invalid');
143
+ const directory = artifactRecoveryIntentRoot(repoRoot);
110
144
  if (!fs.existsSync(directory))
111
145
  return Object.freeze([]);
112
146
  return Object.freeze(fs.readdirSync(directory)
@@ -118,13 +152,13 @@ export function findArtifactRepairIntentsByOperation(repoRoot, operationId) {
118
152
  value = JSON.parse(fs.readFileSync(target, 'utf8'));
119
153
  }
120
154
  catch (error) {
121
- throw new Error(`ARTIFACT_REPAIR_INTENT_INVALID: ${String(error)}`);
155
+ throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`);
122
156
  }
123
- if (!isArtifactRepairIntent(value))
124
- throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
157
+ if (!isArtifactRecoveryIntent(value))
158
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
125
159
  return value;
126
160
  })
127
161
  .filter((value) => value.recoveryOperationId === operationId));
128
162
  }
129
- export { readArtifactRepairIntent };
163
+ export { intentPath as artifactRecoveryIntentPath };
130
164
  //# sourceMappingURL=artifact-repair-intent.js.map