@fitlab-ai/agent-infra 0.10.0 → 0.11.1
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.
- package/README.md +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +22 -12
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +10 -5
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +84 -32
- package/dist/lib/platform/issues.js +3 -4
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +184 -162
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +90 -22
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +6 -3
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +28 -2
- package/dist/lib/task/artifact-recovery.js +114 -68
- package/dist/lib/task/artifact-repair-intent.js +25 -8
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/control-authority.js +1 -1
- package/dist/lib/task/events.js +66 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/intermediate-cleanup.js +28 -3
- package/dist/lib/task/local-artifact-finalization.js +85 -18
- package/dist/lib/task/orchestration.js +8 -6
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +198 -11
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lib/task/workflow-command.js +2 -0
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/internal/task-operation-registry.ts +22 -12
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +9 -5
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +98 -31
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +203 -188
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +1 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +113 -22
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +6 -4
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +28 -2
- package/lib/task/artifact-recovery.ts +120 -67
- package/lib/task/artifact-repair-intent.ts +38 -11
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/control-authority.ts +1 -1
- package/lib/task/events.ts +55 -38
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/intermediate-cleanup.ts +18 -2
- package/lib/task/local-artifact-finalization.ts +91 -15
- package/lib/task/orchestration.ts +12 -7
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +1 -1
- package/lib/task/review-finalization.ts +198 -9
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/lib/task/workflow-command.ts +2 -0
- package/lib/task/workflow-dispatch.ts +171 -0
- package/package.json +1 -1
- package/templates/.agents/rules/issue-sync.en.md +11 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
- package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- package/lib/internal/host-control.ts +0 -28
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { writeDurableFile } from '../fs/durable-write.js';
|
|
5
|
-
import { readStableFileSync } from '
|
|
5
|
+
import { readStableFileSync } from './secure-fs.js';
|
|
6
6
|
import { parseArtifactName } from './artifact-name.js';
|
|
7
7
|
import { canonicalSemanticDigest, sha256Content } from './artifact-operations.js';
|
|
8
8
|
import { artifactRecoveryRoot, readArtifactRecoveryIntent, writeArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
@@ -139,7 +139,7 @@ function contextPaths(tuple, options, recoveryId) {
|
|
|
139
139
|
formalPath: path.join(taskDir, tuple.artifact),
|
|
140
140
|
stagingPath: path.join(root, 'candidate.md'),
|
|
141
141
|
baselinePath: path.join(root, 'baseline.md'),
|
|
142
|
-
|
|
142
|
+
generationsPath: path.join(root, 'generations'),
|
|
143
143
|
baselineSha256: '',
|
|
144
144
|
baselineSemanticDigest: ''
|
|
145
145
|
};
|
|
@@ -167,16 +167,22 @@ function contextWithIntent(context, intent) {
|
|
|
167
167
|
}
|
|
168
168
|
function createIntent(context, now) {
|
|
169
169
|
return {
|
|
170
|
-
version:
|
|
170
|
+
version: 4,
|
|
171
171
|
taskId: context.taskId,
|
|
172
172
|
family: context.family,
|
|
173
173
|
artifact: context.artifact,
|
|
174
174
|
round: context.round,
|
|
175
|
-
state: 'awaiting-recovery',
|
|
175
|
+
state: 'awaiting-preflight-recovery',
|
|
176
176
|
baselineSha256: context.baselineSha256,
|
|
177
177
|
baselineSemanticDigest: context.baselineSemanticDigest,
|
|
178
178
|
stagingId: context.stagingId,
|
|
179
179
|
candidateSha256: context.baselineSha256,
|
|
180
|
+
preflightArtifactSha256: null,
|
|
181
|
+
preflightSemanticDigest: null,
|
|
182
|
+
activeGenerationSha256: null,
|
|
183
|
+
activeGenerationSemanticDigest: null,
|
|
184
|
+
pendingGenerationSha256: null,
|
|
185
|
+
pendingGenerationSemanticDigest: null,
|
|
180
186
|
finalArtifactSha256: null,
|
|
181
187
|
finalSemanticDigest: null,
|
|
182
188
|
recoveryOperationId: context.recoveryId,
|
|
@@ -209,7 +215,7 @@ export function beginArtifactRecovery(tuple, baselineBytes, options) {
|
|
|
209
215
|
fail('ARTIFACT_RECOVERY_BASELINE_MISMATCH', 'formal artifact does not match the recovery baseline');
|
|
210
216
|
}
|
|
211
217
|
const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
|
|
212
|
-
if (existing &&
|
|
218
|
+
if (existing && !['aborted', 'consumed', 'passed'].includes(existing.state)) {
|
|
213
219
|
fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
|
|
214
220
|
}
|
|
215
221
|
writeBytes(prepared.baselinePath, baselineBytes);
|
|
@@ -225,6 +231,7 @@ export function recordArtifactRecoveryPassed(tuple, options) {
|
|
|
225
231
|
assertRecoveryId(context.recoveryId);
|
|
226
232
|
assertRegular(context.formalPath, 'formal artifact');
|
|
227
233
|
return runLocked(context, 'task-artifact.recovery.record-passed', () => {
|
|
234
|
+
assertRecoveryRoot(context);
|
|
228
235
|
const current = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
229
236
|
const prepared = {
|
|
230
237
|
...context,
|
|
@@ -244,18 +251,18 @@ export function recordArtifactRecoveryPassed(tuple, options) {
|
|
|
244
251
|
&& existing.finalSemanticDigest === prepared.baselineSemanticDigest) {
|
|
245
252
|
return recoveryContextFromIntent(prepared.repoRoot, prepared.taskDir, existing);
|
|
246
253
|
}
|
|
247
|
-
if (existing && existing.state !== 'aborted') {
|
|
254
|
+
if (existing && existing.state !== 'aborted' && existing.state !== 'passed') {
|
|
248
255
|
fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
|
|
249
256
|
}
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
};
|
|
258
|
-
|
|
257
|
+
const base = createIntent(prepared, Date.now());
|
|
258
|
+
writeBytes(prepared.baselinePath, current.bytes);
|
|
259
|
+
writeBytes(prepared.stagingPath, current.bytes);
|
|
260
|
+
writeArtifactRecoveryIntent(prepared.repoRoot, base, { expected: existing });
|
|
261
|
+
const staged = stageArtifactCandidate(prepared, current.bytes, { lockAlreadyHeld: true });
|
|
262
|
+
prepareArtifactRecoveryCommit(prepared, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: true });
|
|
263
|
+
commitArtifactRecovery(prepared, { lockAlreadyHeld: true });
|
|
264
|
+
prepareArtifactRecoveryFinal(prepared, current.bytes, { lockAlreadyHeld: true });
|
|
265
|
+
commitArtifactRecovery(prepared, { lockAlreadyHeld: true });
|
|
259
266
|
return prepared;
|
|
260
267
|
}, options.lockAlreadyHeld);
|
|
261
268
|
}
|
|
@@ -263,7 +270,7 @@ export function stageArtifactCandidate(context, candidateBytes, options = {}) {
|
|
|
263
270
|
return runLocked(context, 'task-artifact.recovery.stage', () => {
|
|
264
271
|
assertRecoveryRoot(context);
|
|
265
272
|
const intent = readIntentForContext(context);
|
|
266
|
-
if (intent.state !== 'awaiting-recovery')
|
|
273
|
+
if (intent.state !== 'awaiting-preflight-recovery')
|
|
267
274
|
fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot stage a candidate from '${intent.state}'`);
|
|
268
275
|
const candidateSha256 = sha256Content(candidateBytes.toString('utf8'));
|
|
269
276
|
const semanticDigest = canonicalSemanticDigest(candidateBytes.toString('utf8'));
|
|
@@ -273,30 +280,69 @@ export function stageArtifactCandidate(context, candidateBytes, options = {}) {
|
|
|
273
280
|
return { recoveryId: context.recoveryId, candidateSha256, semanticDigest, path: context.stagingPath };
|
|
274
281
|
}, options.lockAlreadyHeld);
|
|
275
282
|
}
|
|
283
|
+
function generationPath(context, sha256) {
|
|
284
|
+
assertDigest(sha256, 'generation digest');
|
|
285
|
+
return path.join(context.generationsPath, `${sha256}.md`);
|
|
286
|
+
}
|
|
287
|
+
/** Persist and verify an immutable digest-addressed snapshot before it is journaled. */
|
|
288
|
+
function writeGeneration(context, bytes, sha256, semanticDigest) {
|
|
289
|
+
ensureOwnedDirectoryTree(context.repoRoot, context.generationsPath);
|
|
290
|
+
const target = generationPath(context, sha256);
|
|
291
|
+
if (fs.existsSync(target)) {
|
|
292
|
+
const existing = readStableFileSync(target, { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
|
|
293
|
+
if (canonicalSemanticDigest(existing.bytes.toString('utf8')) !== semanticDigest) {
|
|
294
|
+
fail('ARTIFACT_RECOVERY_CONFLICT', 'existing generation does not match its semantic digest');
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
writeBytes(target, bytes);
|
|
299
|
+
fs.chmodSync(target, 0o400);
|
|
300
|
+
const sealed = readStableFileSync(target, { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
|
|
301
|
+
if (canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== semanticDigest) {
|
|
302
|
+
fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'generation does not match its validated semantic digest');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function publishGeneration(context, sha256, semanticDigest, baselineSha256) {
|
|
306
|
+
const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
307
|
+
const generation = readStableFileSync(generationPath(context, sha256), { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
|
|
308
|
+
if (canonicalSemanticDigest(generation.bytes.toString('utf8')) !== semanticDigest) {
|
|
309
|
+
fail('ARTIFACT_RECOVERY_CONFLICT', 'generation semantic digest does not match the journal');
|
|
310
|
+
}
|
|
311
|
+
if (target.sha256 === sha256)
|
|
312
|
+
return;
|
|
313
|
+
if (target.sha256 !== baselineSha256)
|
|
314
|
+
fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact changed outside the expected generation');
|
|
315
|
+
const publishPath = path.join(path.dirname(context.stagingPath), 'publish.md');
|
|
316
|
+
unlinkIfPresent(publishPath);
|
|
317
|
+
writeBytes(publishPath, generation.bytes);
|
|
318
|
+
fs.chmodSync(publishPath, 0o400);
|
|
319
|
+
fs.renameSync(publishPath, context.formalPath);
|
|
320
|
+
const published = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES, expectedSha256: sha256 });
|
|
321
|
+
if (canonicalSemanticDigest(published.bytes.toString('utf8')) !== semanticDigest) {
|
|
322
|
+
fail('ARTIFACT_RECOVERY_CONFLICT', 'published artifact does not match its generation');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
276
325
|
export function prepareArtifactRecoveryCommit(context, finalSha256, finalSemanticDigest, options = {}) {
|
|
277
326
|
assertDigest(finalSha256, 'final artifact digest');
|
|
278
327
|
assertDigest(finalSemanticDigest, 'final semantic digest');
|
|
279
328
|
return runLocked(context, 'task-artifact.recovery.prepare', () => {
|
|
280
329
|
const intent = readIntentForContext(context);
|
|
281
|
-
if (intent.state !== 'awaiting-recovery')
|
|
282
|
-
fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a
|
|
330
|
+
if (intent.state !== 'awaiting-preflight-recovery')
|
|
331
|
+
fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a preflight from '${intent.state}'`);
|
|
283
332
|
assertRecoveryRoot(context);
|
|
284
333
|
const staged = readStableFileSync(context.stagingPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
285
334
|
if (staged.sha256 !== finalSha256 || canonicalSemanticDigest(staged.bytes.toString('utf8')) !== finalSemanticDigest) {
|
|
286
335
|
fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'staged candidate does not match the finalizer digest');
|
|
287
336
|
}
|
|
288
|
-
|
|
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
|
-
}
|
|
337
|
+
writeGeneration(context, staged.bytes, finalSha256, finalSemanticDigest);
|
|
294
338
|
const next = {
|
|
295
339
|
...intent,
|
|
296
|
-
state: '
|
|
340
|
+
state: 'preflight-ready',
|
|
297
341
|
candidateSha256: staged.sha256,
|
|
298
|
-
|
|
299
|
-
finalSemanticDigest,
|
|
342
|
+
preflightArtifactSha256: finalSha256,
|
|
343
|
+
preflightSemanticDigest: finalSemanticDigest,
|
|
344
|
+
activeGenerationSha256: finalSha256,
|
|
345
|
+
activeGenerationSemanticDigest: finalSemanticDigest,
|
|
300
346
|
errorCode: null,
|
|
301
347
|
errorMessage: null,
|
|
302
348
|
updatedAt: Date.now()
|
|
@@ -306,11 +352,11 @@ export function prepareArtifactRecoveryCommit(context, finalSha256, finalSemanti
|
|
|
306
352
|
}, options.lockAlreadyHeld);
|
|
307
353
|
}
|
|
308
354
|
function markCommitStarted(context, intent) {
|
|
309
|
-
if (intent.state === 'commit-started')
|
|
355
|
+
if (intent.state === 'preflight-commit-started' || intent.state === 'commit-started')
|
|
310
356
|
return intent;
|
|
311
|
-
if (intent.state !== '
|
|
357
|
+
if (intent.state !== 'preflight-ready' && intent.state !== 'full-finalizer-ready')
|
|
312
358
|
fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot commit from '${intent.state}'`);
|
|
313
|
-
const next = { ...intent, state: 'commit-started', updatedAt: Date.now() };
|
|
359
|
+
const next = { ...intent, state: intent.state === 'preflight-ready' ? 'preflight-commit-started' : 'commit-started', updatedAt: Date.now() };
|
|
314
360
|
writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
|
|
315
361
|
return next;
|
|
316
362
|
}
|
|
@@ -321,44 +367,41 @@ export function commitArtifactRecovery(context, options = {}) {
|
|
|
321
367
|
return intent;
|
|
322
368
|
assertRecoveryRoot(context);
|
|
323
369
|
intent = markCommitStarted(context, intent);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const passed = { ...intent, state: 'passed', updatedAt: Date.now() };
|
|
370
|
+
if (intent.state === 'preflight-commit-started') {
|
|
371
|
+
publishGeneration(context, intent.activeGenerationSha256, intent.activeGenerationSemanticDigest, intent.baselineSha256);
|
|
372
|
+
const passed = { ...intent, state: 'preflight-passed', updatedAt: Date.now() };
|
|
327
373
|
writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
|
|
328
374
|
return passed;
|
|
329
375
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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() };
|
|
376
|
+
publishGeneration(context, intent.pendingGenerationSha256, intent.pendingGenerationSemanticDigest, intent.activeGenerationSha256);
|
|
377
|
+
options.afterPublish?.();
|
|
378
|
+
const passed = {
|
|
379
|
+
...intent, state: 'passed', activeGenerationSha256: intent.pendingGenerationSha256,
|
|
380
|
+
activeGenerationSemanticDigest: intent.pendingGenerationSemanticDigest,
|
|
381
|
+
pendingGenerationSha256: null, pendingGenerationSemanticDigest: null, updatedAt: Date.now()
|
|
382
|
+
};
|
|
358
383
|
writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
|
|
359
384
|
return passed;
|
|
360
385
|
}, options.lockAlreadyHeld);
|
|
361
386
|
}
|
|
387
|
+
export function prepareArtifactRecoveryFinal(context, finalBytes, options = {}) {
|
|
388
|
+
return runLocked(context, 'task-artifact.recovery.finalize', () => {
|
|
389
|
+
const intent = readIntentForContext(context);
|
|
390
|
+
if (intent.state !== 'preflight-passed')
|
|
391
|
+
fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot finalize from '${intent.state}'`);
|
|
392
|
+
const finalSha256 = sha256Content(finalBytes.toString('utf8'));
|
|
393
|
+
const finalSemanticDigest = canonicalSemanticDigest(finalBytes.toString('utf8'));
|
|
394
|
+
if (finalSha256 !== intent.activeGenerationSha256)
|
|
395
|
+
writeGeneration(context, finalBytes, finalSha256, finalSemanticDigest);
|
|
396
|
+
const next = {
|
|
397
|
+
...intent, state: 'full-finalizer-ready', pendingGenerationSha256: finalSha256,
|
|
398
|
+
pendingGenerationSemanticDigest: finalSemanticDigest, finalArtifactSha256: finalSha256,
|
|
399
|
+
finalSemanticDigest, updatedAt: Date.now()
|
|
400
|
+
};
|
|
401
|
+
writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
|
|
402
|
+
return next;
|
|
403
|
+
}, options.lockAlreadyHeld);
|
|
404
|
+
}
|
|
362
405
|
export function abortArtifactRecovery(context, code, message, options = {}) {
|
|
363
406
|
return runLocked(context, 'task-artifact.recovery.abort', () => {
|
|
364
407
|
const intent = readIntentForContext(context);
|
|
@@ -383,16 +426,19 @@ export function abortArtifactRecovery(context, code, message, options = {}) {
|
|
|
383
426
|
export function reconcileArtifactRecovery(context, options = {}) {
|
|
384
427
|
return runLocked(context, 'task-artifact.recovery.reconcile', () => {
|
|
385
428
|
const intent = readIntentForContext(context);
|
|
386
|
-
if (intent.state !== 'commit-started')
|
|
429
|
+
if (intent.state !== 'preflight-commit-started' && intent.state !== 'commit-started')
|
|
387
430
|
return { status: intent.state, intent };
|
|
388
431
|
const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
432
|
+
const expected = intent.state === 'preflight-commit-started' ? intent.activeGenerationSha256 : intent.pendingGenerationSha256;
|
|
433
|
+
if (target.sha256 === expected && (options.validateFinal?.() ?? true)) {
|
|
434
|
+
const next = intent.state === 'preflight-commit-started'
|
|
435
|
+
? { ...intent, state: 'preflight-passed', updatedAt: Date.now() }
|
|
436
|
+
: { ...intent, state: 'passed', activeGenerationSha256: intent.pendingGenerationSha256, activeGenerationSemanticDigest: intent.pendingGenerationSemanticDigest, pendingGenerationSha256: null, pendingGenerationSemanticDigest: null, updatedAt: Date.now() };
|
|
437
|
+
writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
|
|
438
|
+
return { status: next.state, intent: next };
|
|
393
439
|
}
|
|
394
|
-
if (target.sha256 === intent.baselineSha256) {
|
|
395
|
-
const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true });
|
|
440
|
+
if (target.sha256 === (intent.state === 'preflight-commit-started' ? intent.baselineSha256 : intent.activeGenerationSha256)) {
|
|
441
|
+
const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true, afterPublish: options.afterPublish });
|
|
396
442
|
return { status: retried.state, intent: retried };
|
|
397
443
|
}
|
|
398
444
|
if (options.restoreBaseline) {
|
|
@@ -37,22 +37,28 @@ export function isArtifactRecoveryIntent(value) {
|
|
|
37
37
|
return false;
|
|
38
38
|
const intent = value;
|
|
39
39
|
return exactKeys(intent, [
|
|
40
|
-
'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
|
|
40
|
+
'activeGenerationSemanticDigest', 'activeGenerationSha256', 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
|
|
41
41
|
'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
|
|
42
|
-
'phase', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
|
|
42
|
+
'pendingGenerationSemanticDigest', 'pendingGenerationSha256', 'phase', 'preflightArtifactSha256', 'preflightSemanticDigest', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
|
|
43
43
|
'version'
|
|
44
44
|
])
|
|
45
|
-
&& intent.version ===
|
|
45
|
+
&& intent.version === 4
|
|
46
46
|
&& validTaskId(intent.taskId)
|
|
47
47
|
&& typeof intent.family === 'string'
|
|
48
48
|
&& ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
|
|
49
49
|
&& Number.isSafeInteger(intent.round) && intent.round > 0
|
|
50
50
|
&& validArtifact(intent.artifact, intent.family, intent.round)
|
|
51
|
-
&& ['awaiting-recovery', '
|
|
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
52
|
&& validDigest(intent.baselineSha256)
|
|
53
53
|
&& validDigest(intent.baselineSemanticDigest)
|
|
54
54
|
&& validRecoveryId(intent.stagingId)
|
|
55
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))
|
|
56
62
|
&& (intent.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
|
|
57
63
|
&& (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
|
|
58
64
|
&& validRecoveryId(intent.recoveryOperationId)
|
|
@@ -64,11 +70,22 @@ export function isArtifactRecoveryIntent(value) {
|
|
|
64
70
|
&& (intent.errorMessage === null || validToken(intent.errorMessage))
|
|
65
71
|
&& Number.isSafeInteger(intent.createdAt)
|
|
66
72
|
&& Number.isSafeInteger(intent.updatedAt)
|
|
67
|
-
&& (intent.state === 'awaiting-recovery'
|
|
68
|
-
? intent.
|
|
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
|
|
69
78
|
: true)
|
|
70
|
-
&& (intent.state === '
|
|
71
|
-
? validDigest(intent.candidateSha256) && validDigest(intent.
|
|
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)
|
|
72
89
|
: true)
|
|
73
90
|
&& (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
|
|
74
91
|
}
|
|
@@ -146,8 +146,6 @@ function qualificationRecoveryAction(facts) {
|
|
|
146
146
|
return QUALIFICATION_RECOVERY_ORDER.find(({ pattern }) => stale.some((name) => pattern.test(name)))?.action ?? null;
|
|
147
147
|
}
|
|
148
148
|
function recommendNext(facts) {
|
|
149
|
-
if (invalidationBlocks(facts.invalidation))
|
|
150
|
-
return { action: null, reasonCode: 'INVALIDATION_INCOMPLETE', evidence: ['reconcile task invalidation before routing'] };
|
|
151
149
|
if (facts.qualificationStale) {
|
|
152
150
|
const action = qualificationRecoveryAction(facts);
|
|
153
151
|
return {
|
|
@@ -266,7 +266,7 @@ function lifecycleRecoveryIntentMatches(intent, selector) {
|
|
|
266
266
|
return intent.taskId === selector.taskId
|
|
267
267
|
&& intent.family === selector.family
|
|
268
268
|
&& intent.artifact === selector.artifact
|
|
269
|
-
&& (intent.state === 'commit-started' || intent.state === 'consumed');
|
|
269
|
+
&& (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
|
|
270
270
|
}
|
|
271
271
|
/**
|
|
272
272
|
* Completes only the durable tail of a committed task event after a process
|