@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
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
5
5
|
import { writeDurableFile } from '../fs/durable-write.ts';
|
|
6
|
-
import { readStableFileSync } from '
|
|
6
|
+
import { readStableFileSync } from './secure-fs.ts';
|
|
7
7
|
import { parseArtifactName } from './artifact-name.ts';
|
|
8
8
|
import { canonicalSemanticDigest, sha256Content } from './artifact-operations.ts';
|
|
9
9
|
import {
|
|
@@ -39,7 +39,7 @@ export type ArtifactRecoveryContext = Readonly<ArtifactRecoveryTuple & {
|
|
|
39
39
|
formalPath: string;
|
|
40
40
|
stagingPath: string;
|
|
41
41
|
baselinePath: string;
|
|
42
|
-
|
|
42
|
+
generationsPath: string;
|
|
43
43
|
baselineSha256: string;
|
|
44
44
|
baselineSemanticDigest: string;
|
|
45
45
|
}>;
|
|
@@ -191,7 +191,7 @@ function contextPaths(
|
|
|
191
191
|
formalPath: path.join(taskDir, tuple.artifact),
|
|
192
192
|
stagingPath: path.join(root, 'candidate.md'),
|
|
193
193
|
baselinePath: path.join(root, 'baseline.md'),
|
|
194
|
-
|
|
194
|
+
generationsPath: path.join(root, 'generations'),
|
|
195
195
|
baselineSha256: '',
|
|
196
196
|
baselineSemanticDigest: ''
|
|
197
197
|
};
|
|
@@ -221,16 +221,22 @@ function contextWithIntent(context: ArtifactRecoveryContext, intent: ArtifactRec
|
|
|
221
221
|
|
|
222
222
|
function createIntent(context: ArtifactRecoveryContext, now: number): ArtifactRecoveryIntent {
|
|
223
223
|
return {
|
|
224
|
-
version:
|
|
224
|
+
version: 4,
|
|
225
225
|
taskId: context.taskId,
|
|
226
226
|
family: context.family,
|
|
227
227
|
artifact: context.artifact,
|
|
228
228
|
round: context.round,
|
|
229
|
-
state: 'awaiting-recovery',
|
|
229
|
+
state: 'awaiting-preflight-recovery',
|
|
230
230
|
baselineSha256: context.baselineSha256,
|
|
231
231
|
baselineSemanticDigest: context.baselineSemanticDigest,
|
|
232
232
|
stagingId: context.stagingId,
|
|
233
233
|
candidateSha256: context.baselineSha256,
|
|
234
|
+
preflightArtifactSha256: null,
|
|
235
|
+
preflightSemanticDigest: null,
|
|
236
|
+
activeGenerationSha256: null,
|
|
237
|
+
activeGenerationSemanticDigest: null,
|
|
238
|
+
pendingGenerationSha256: null,
|
|
239
|
+
pendingGenerationSemanticDigest: null,
|
|
234
240
|
finalArtifactSha256: null,
|
|
235
241
|
finalSemanticDigest: null,
|
|
236
242
|
recoveryOperationId: context.recoveryId,
|
|
@@ -267,7 +273,7 @@ export function beginArtifactRecovery(
|
|
|
267
273
|
fail('ARTIFACT_RECOVERY_BASELINE_MISMATCH', 'formal artifact does not match the recovery baseline');
|
|
268
274
|
}
|
|
269
275
|
const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
|
|
270
|
-
if (existing &&
|
|
276
|
+
if (existing && !['aborted', 'consumed', 'passed'].includes(existing.state)) {
|
|
271
277
|
fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
|
|
272
278
|
}
|
|
273
279
|
writeBytes(prepared.baselinePath, baselineBytes);
|
|
@@ -287,6 +293,7 @@ export function recordArtifactRecoveryPassed(
|
|
|
287
293
|
assertRecoveryId(context.recoveryId);
|
|
288
294
|
assertRegular(context.formalPath, 'formal artifact');
|
|
289
295
|
return runLocked(context, 'task-artifact.recovery.record-passed', () => {
|
|
296
|
+
assertRecoveryRoot(context);
|
|
290
297
|
const current = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
291
298
|
const prepared = {
|
|
292
299
|
...context,
|
|
@@ -306,18 +313,18 @@ export function recordArtifactRecoveryPassed(
|
|
|
306
313
|
&& existing.finalSemanticDigest === prepared.baselineSemanticDigest) {
|
|
307
314
|
return recoveryContextFromIntent(prepared.repoRoot, prepared.taskDir, existing);
|
|
308
315
|
}
|
|
309
|
-
if (existing && existing.state !== 'aborted') {
|
|
316
|
+
if (existing && existing.state !== 'aborted' && existing.state !== 'passed') {
|
|
310
317
|
fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
|
|
311
318
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
};
|
|
320
|
-
|
|
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 });
|
|
321
328
|
return prepared;
|
|
322
329
|
}, options.lockAlreadyHeld);
|
|
323
330
|
}
|
|
@@ -330,7 +337,7 @@ export function stageArtifactCandidate(
|
|
|
330
337
|
return runLocked(context, 'task-artifact.recovery.stage', () => {
|
|
331
338
|
assertRecoveryRoot(context);
|
|
332
339
|
const intent = readIntentForContext(context);
|
|
333
|
-
if (intent.state !== 'awaiting-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot stage a candidate from '${intent.state}'`);
|
|
340
|
+
if (intent.state !== 'awaiting-preflight-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot stage a candidate from '${intent.state}'`);
|
|
334
341
|
const candidateSha256 = sha256Content(candidateBytes.toString('utf8'));
|
|
335
342
|
const semanticDigest = canonicalSemanticDigest(candidateBytes.toString('utf8'));
|
|
336
343
|
writeBytes(context.stagingPath, candidateBytes);
|
|
@@ -340,6 +347,49 @@ export function stageArtifactCandidate(
|
|
|
340
347
|
}, options.lockAlreadyHeld);
|
|
341
348
|
}
|
|
342
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
|
+
|
|
343
393
|
export function prepareArtifactRecoveryCommit(
|
|
344
394
|
context: ArtifactRecoveryContext,
|
|
345
395
|
finalSha256: string,
|
|
@@ -350,24 +400,21 @@ export function prepareArtifactRecoveryCommit(
|
|
|
350
400
|
assertDigest(finalSemanticDigest, 'final semantic digest');
|
|
351
401
|
return runLocked(context, 'task-artifact.recovery.prepare', () => {
|
|
352
402
|
const intent = readIntentForContext(context);
|
|
353
|
-
if (intent.state !== 'awaiting-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a
|
|
403
|
+
if (intent.state !== 'awaiting-preflight-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a preflight from '${intent.state}'`);
|
|
354
404
|
assertRecoveryRoot(context);
|
|
355
405
|
const staged = readStableFileSync(context.stagingPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
356
406
|
if (staged.sha256 !== finalSha256 || canonicalSemanticDigest(staged.bytes.toString('utf8')) !== finalSemanticDigest) {
|
|
357
407
|
fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'staged candidate does not match the finalizer digest');
|
|
358
408
|
}
|
|
359
|
-
|
|
360
|
-
fs.chmodSync(context.finalPath, 0o400);
|
|
361
|
-
const sealed = readStableFileSync(context.finalPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
362
|
-
if (sealed.sha256 !== finalSha256 || canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== finalSemanticDigest) {
|
|
363
|
-
fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'sealed candidate does not match the finalizer digest');
|
|
364
|
-
}
|
|
409
|
+
writeGeneration(context, staged.bytes, finalSha256, finalSemanticDigest);
|
|
365
410
|
const next: ArtifactRecoveryIntent = {
|
|
366
411
|
...intent,
|
|
367
|
-
state: '
|
|
412
|
+
state: 'preflight-ready',
|
|
368
413
|
candidateSha256: staged.sha256,
|
|
369
|
-
|
|
370
|
-
finalSemanticDigest,
|
|
414
|
+
preflightArtifactSha256: finalSha256,
|
|
415
|
+
preflightSemanticDigest: finalSemanticDigest,
|
|
416
|
+
activeGenerationSha256: finalSha256,
|
|
417
|
+
activeGenerationSemanticDigest: finalSemanticDigest,
|
|
371
418
|
errorCode: null,
|
|
372
419
|
errorMessage: null,
|
|
373
420
|
updatedAt: Date.now()
|
|
@@ -378,59 +425,61 @@ export function prepareArtifactRecoveryCommit(
|
|
|
378
425
|
}
|
|
379
426
|
|
|
380
427
|
function markCommitStarted(context: ArtifactRecoveryContext, intent: ArtifactRecoveryIntent): ArtifactRecoveryIntent {
|
|
381
|
-
if (intent.state === 'commit-started') return intent;
|
|
382
|
-
if (intent.state !== '
|
|
383
|
-
const next: ArtifactRecoveryIntent = { ...intent, state: 'commit-started', updatedAt: Date.now() };
|
|
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() };
|
|
384
431
|
writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
|
|
385
432
|
return next;
|
|
386
433
|
}
|
|
387
434
|
|
|
388
435
|
export function commitArtifactRecovery(
|
|
389
436
|
context: ArtifactRecoveryContext,
|
|
390
|
-
options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
|
|
437
|
+
options: Readonly<{ lockAlreadyHeld?: boolean; afterPublish?: () => void }> = {}
|
|
391
438
|
): ArtifactRecoveryIntent {
|
|
392
439
|
return runLocked(context, 'task-artifact.recovery.commit', () => {
|
|
393
440
|
let intent = readIntentForContext(context);
|
|
394
441
|
if (intent.state === 'passed' || intent.state === 'consumed') return intent;
|
|
395
442
|
assertRecoveryRoot(context);
|
|
396
443
|
intent = markCommitStarted(context, intent);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const passed
|
|
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() };
|
|
400
447
|
writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
|
|
401
448
|
return passed;
|
|
402
449
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
// Publish the bytes that were validated above. The sealed path is only a
|
|
412
|
-
// recovery input; renaming it directly would reopen a same-uid path swap
|
|
413
|
-
// between validation and publication.
|
|
414
|
-
const publishPath = path.join(path.dirname(context.finalPath), 'publish.md');
|
|
415
|
-
unlinkIfPresent(publishPath);
|
|
416
|
-
writeBytes(publishPath, sealed.bytes);
|
|
417
|
-
fs.chmodSync(publishPath, 0o400);
|
|
418
|
-
const publish = readStableFileSync(publishPath, {
|
|
419
|
-
maxBytes: MAX_ARTIFACT_BYTES,
|
|
420
|
-
expectedSha256: intent.finalArtifactSha256!
|
|
421
|
-
});
|
|
422
|
-
if (canonicalSemanticDigest(publish.bytes.toString('utf8')) !== intent.finalSemanticDigest) {
|
|
423
|
-
fail('ARTIFACT_RECOVERY_CONFLICT', 'publish artifact does not match the staged final semantic digest');
|
|
424
|
-
}
|
|
425
|
-
fs.renameSync(publishPath, context.formalPath);
|
|
426
|
-
const published = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
427
|
-
if (published.sha256 !== intent.finalArtifactSha256) fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact does not match the staged final digest');
|
|
428
|
-
const passed: ArtifactRecoveryIntent = { ...intent, state: 'passed', updatedAt: Date.now() };
|
|
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
|
+
};
|
|
429
457
|
writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
|
|
430
458
|
return passed;
|
|
431
459
|
}, options.lockAlreadyHeld);
|
|
432
460
|
}
|
|
433
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
|
+
|
|
434
483
|
export function abortArtifactRecovery(
|
|
435
484
|
context: ArtifactRecoveryContext,
|
|
436
485
|
code: string,
|
|
@@ -462,19 +511,23 @@ export function reconcileArtifactRecovery(
|
|
|
462
511
|
lockAlreadyHeld?: boolean;
|
|
463
512
|
validateFinal?: () => boolean;
|
|
464
513
|
restoreBaseline?: boolean;
|
|
514
|
+
afterPublish?: () => void;
|
|
465
515
|
}> = {}
|
|
466
516
|
): ArtifactRecoveryReconcileResult {
|
|
467
517
|
return runLocked(context, 'task-artifact.recovery.reconcile', () => {
|
|
468
518
|
const intent = readIntentForContext(context);
|
|
469
|
-
if (intent.state !== 'commit-started') return { status: intent.state, intent };
|
|
519
|
+
if (intent.state !== 'preflight-commit-started' && intent.state !== 'commit-started') return { status: intent.state, intent };
|
|
470
520
|
const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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 };
|
|
475
528
|
}
|
|
476
|
-
if (target.sha256 === intent.baselineSha256) {
|
|
477
|
-
const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true });
|
|
529
|
+
if (target.sha256 === (intent.state === 'preflight-commit-started' ? intent.baselineSha256 : intent.activeGenerationSha256)) {
|
|
530
|
+
const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true, afterPublish: options.afterPublish });
|
|
478
531
|
return { status: retried.state, intent: retried };
|
|
479
532
|
}
|
|
480
533
|
if (options.restoreBaseline) {
|
|
@@ -10,15 +10,19 @@ export type ArtifactRecoveryPhase =
|
|
|
10
10
|
| 'artifact.finalize-local'
|
|
11
11
|
| 'task-event.completed';
|
|
12
12
|
export type ArtifactRecoveryState =
|
|
13
|
-
| 'awaiting-recovery'
|
|
14
|
-
| '
|
|
13
|
+
| 'awaiting-preflight-recovery'
|
|
14
|
+
| 'preflight-ready'
|
|
15
|
+
| 'preflight-commit-started'
|
|
16
|
+
| 'preflight-passed'
|
|
17
|
+
| 'full-finalizer-ready'
|
|
15
18
|
| 'commit-started'
|
|
16
19
|
| 'passed'
|
|
20
|
+
| 'consumption-started'
|
|
17
21
|
| 'consumed'
|
|
18
22
|
| 'aborted';
|
|
19
23
|
|
|
20
24
|
export type ArtifactRecoveryIntent = Readonly<{
|
|
21
|
-
version:
|
|
25
|
+
version: 4;
|
|
22
26
|
taskId: string;
|
|
23
27
|
family: ArtifactSchemaFamily;
|
|
24
28
|
artifact: string;
|
|
@@ -28,6 +32,12 @@ export type ArtifactRecoveryIntent = Readonly<{
|
|
|
28
32
|
baselineSemanticDigest: string;
|
|
29
33
|
stagingId: string;
|
|
30
34
|
candidateSha256: string | null;
|
|
35
|
+
preflightArtifactSha256: string | null;
|
|
36
|
+
preflightSemanticDigest: string | null;
|
|
37
|
+
activeGenerationSha256: string | null;
|
|
38
|
+
activeGenerationSemanticDigest: string | null;
|
|
39
|
+
pendingGenerationSha256: string | null;
|
|
40
|
+
pendingGenerationSemanticDigest: string | null;
|
|
31
41
|
finalArtifactSha256: string | null;
|
|
32
42
|
finalSemanticDigest: string | null;
|
|
33
43
|
recoveryOperationId: string;
|
|
@@ -82,22 +92,28 @@ export function isArtifactRecoveryIntent(value: unknown): value is ArtifactRecov
|
|
|
82
92
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
83
93
|
const intent = value as Record<string, unknown>;
|
|
84
94
|
return exactKeys(intent, [
|
|
85
|
-
'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
|
|
95
|
+
'activeGenerationSemanticDigest', 'activeGenerationSha256', 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
|
|
86
96
|
'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
|
|
87
|
-
'phase', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
|
|
97
|
+
'pendingGenerationSemanticDigest', 'pendingGenerationSha256', 'phase', 'preflightArtifactSha256', 'preflightSemanticDigest', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
|
|
88
98
|
'version'
|
|
89
99
|
])
|
|
90
|
-
&& intent.version ===
|
|
100
|
+
&& intent.version === 4
|
|
91
101
|
&& validTaskId(intent.taskId)
|
|
92
102
|
&& typeof intent.family === 'string'
|
|
93
103
|
&& ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
|
|
94
104
|
&& Number.isSafeInteger(intent.round) && (intent.round as number) > 0
|
|
95
105
|
&& validArtifact(intent.artifact, intent.family, intent.round)
|
|
96
|
-
&& ['awaiting-recovery', '
|
|
106
|
+
&& ['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started', 'passed', 'consumption-started', 'consumed', 'aborted'].includes(String(intent.state))
|
|
97
107
|
&& validDigest(intent.baselineSha256)
|
|
98
108
|
&& validDigest(intent.baselineSemanticDigest)
|
|
99
109
|
&& validRecoveryId(intent.stagingId)
|
|
100
110
|
&& (intent.candidateSha256 === null || validDigest(intent.candidateSha256))
|
|
111
|
+
&& (intent.preflightArtifactSha256 === null || validDigest(intent.preflightArtifactSha256))
|
|
112
|
+
&& (intent.preflightSemanticDigest === null || validDigest(intent.preflightSemanticDigest))
|
|
113
|
+
&& (intent.activeGenerationSha256 === null || validDigest(intent.activeGenerationSha256))
|
|
114
|
+
&& (intent.activeGenerationSemanticDigest === null || validDigest(intent.activeGenerationSemanticDigest))
|
|
115
|
+
&& (intent.pendingGenerationSha256 === null || validDigest(intent.pendingGenerationSha256))
|
|
116
|
+
&& (intent.pendingGenerationSemanticDigest === null || validDigest(intent.pendingGenerationSemanticDigest))
|
|
101
117
|
&& (intent.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
|
|
102
118
|
&& (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
|
|
103
119
|
&& validRecoveryId(intent.recoveryOperationId)
|
|
@@ -109,11 +125,22 @@ export function isArtifactRecoveryIntent(value: unknown): value is ArtifactRecov
|
|
|
109
125
|
&& (intent.errorMessage === null || validToken(intent.errorMessage))
|
|
110
126
|
&& Number.isSafeInteger(intent.createdAt)
|
|
111
127
|
&& Number.isSafeInteger(intent.updatedAt)
|
|
112
|
-
&& (intent.state === 'awaiting-recovery'
|
|
113
|
-
? intent.
|
|
128
|
+
&& (intent.state === 'awaiting-preflight-recovery'
|
|
129
|
+
? intent.preflightArtifactSha256 === null && intent.preflightSemanticDigest === null
|
|
130
|
+
&& intent.activeGenerationSha256 === null && intent.activeGenerationSemanticDigest === null
|
|
131
|
+
&& intent.pendingGenerationSha256 === null && intent.pendingGenerationSemanticDigest === null
|
|
132
|
+
&& intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
|
|
114
133
|
: true)
|
|
115
|
-
&& (intent.state === '
|
|
116
|
-
? validDigest(intent.candidateSha256) && validDigest(intent.
|
|
134
|
+
&& (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'
|
|
135
|
+
? validDigest(intent.candidateSha256) && validDigest(intent.preflightArtifactSha256) && validDigest(intent.preflightSemanticDigest)
|
|
136
|
+
&& validDigest(intent.activeGenerationSha256) && validDigest(intent.activeGenerationSemanticDigest)
|
|
137
|
+
: true)
|
|
138
|
+
&& (intent.state === 'full-finalizer-ready' || intent.state === 'commit-started'
|
|
139
|
+
? validDigest(intent.pendingGenerationSha256) && validDigest(intent.pendingGenerationSemanticDigest)
|
|
140
|
+
&& validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
|
|
141
|
+
: true)
|
|
142
|
+
&& (intent.state === 'passed' || intent.state === 'consumption-started' || intent.state === 'consumed'
|
|
143
|
+
? validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
|
|
117
144
|
: true)
|
|
118
145
|
&& (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
|
|
119
146
|
}
|
package/lib/task/capabilities.ts
CHANGED
|
@@ -187,7 +187,6 @@ function qualificationRecoveryAction(facts: LifecycleFacts): LifecycleAction | n
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
190
|
-
if (invalidationBlocks(facts.invalidation)) return { action: null, reasonCode: 'INVALIDATION_INCOMPLETE', evidence: ['reconcile task invalidation before routing'] };
|
|
191
190
|
if (facts.qualificationStale) {
|
|
192
191
|
const action = qualificationRecoveryAction(facts);
|
|
193
192
|
return {
|
|
@@ -428,7 +428,7 @@ function lifecycleRecoveryIntentMatches(
|
|
|
428
428
|
return intent.taskId === selector.taskId
|
|
429
429
|
&& intent.family === selector.family
|
|
430
430
|
&& intent.artifact === selector.artifact
|
|
431
|
-
&& (intent.state === 'commit-started' || intent.state === 'consumed');
|
|
431
|
+
&& (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
/**
|