@fitlab-ai/agent-infra 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +43 -9
- 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 +83 -21
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +6 -3
- package/dist/lib/task/artifact-operations.js +1 -1
- package/dist/lib/task/artifact-recovery.js +114 -68
- package/dist/lib/task/artifact-repair-intent.js +25 -8
- package/dist/lib/task/control-authority.js +1 -1
- package/dist/lib/task/events.js +6 -6
- package/dist/lib/task/intermediate-cleanup.js +28 -3
- package/dist/lib/task/local-artifact-finalization.js +78 -14
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +203 -10
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- 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/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/task-comment-projection.ts +79 -0
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +46 -13
- 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 +102 -21
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +6 -4
- package/lib/task/artifact-operations.ts +1 -1
- package/lib/task/artifact-recovery.ts +120 -67
- package/lib/task/artifact-repair-intent.ts +38 -11
- package/lib/task/control-authority.ts +1 -1
- package/lib/task/events.ts +6 -6
- package/lib/task/intermediate-cleanup.ts +18 -2
- package/lib/task/local-artifact-finalization.ts +85 -11
- package/lib/task/review-command.ts +1 -1
- package/lib/task/review-finalization.ts +203 -8
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- 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 +9 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +10 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +7 -6
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +10 -8
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +9 -4
- 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-code/SKILL.en.md +3 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- 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
|
@@ -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
|
}
|
|
@@ -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
|
/**
|
package/lib/task/events.ts
CHANGED
|
@@ -402,7 +402,7 @@ function reconcileReviewCompletionReplay(
|
|
|
402
402
|
}
|
|
403
403
|
intent = reconciled.intent;
|
|
404
404
|
}
|
|
405
|
-
if (!['passed', 'consumed'].includes(intent.state)) {
|
|
405
|
+
if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
|
|
406
406
|
return {
|
|
407
407
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
408
408
|
message: `review finalizer provenance is incomplete for ${artifact}`
|
|
@@ -423,7 +423,7 @@ function reconcileReviewCompletionReplay(
|
|
|
423
423
|
message: `review finalizer provenance does not match ${artifact}`
|
|
424
424
|
};
|
|
425
425
|
}
|
|
426
|
-
if (intent.state === 'passed') {
|
|
426
|
+
if (intent.state === 'passed' || intent.state === 'consumption-started') {
|
|
427
427
|
const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
428
428
|
writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
|
|
429
429
|
}
|
|
@@ -963,7 +963,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
963
963
|
);
|
|
964
964
|
if (reconciled.status === 'passed' || reconciled.status === 'consumed') reviewFinalizationIntent = reconciled.intent;
|
|
965
965
|
}
|
|
966
|
-
if (!reviewFinalizationIntent || !['passed', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
966
|
+
if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
967
967
|
return failed(normalized, {
|
|
968
968
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
969
969
|
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
@@ -1116,7 +1116,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1116
1116
|
const expectedIntent = localFinalizationIntent;
|
|
1117
1117
|
localFinalizationIntent = {
|
|
1118
1118
|
...localFinalizationIntent,
|
|
1119
|
-
state: '
|
|
1119
|
+
state: 'consumption-started',
|
|
1120
1120
|
phase: 'task-event.completed',
|
|
1121
1121
|
authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
|
|
1122
1122
|
requestId: lifecycleAuthority.lifecycleRequestId,
|
|
@@ -1146,7 +1146,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1146
1146
|
const expectedIntent = reviewFinalizationIntent;
|
|
1147
1147
|
reviewFinalizationIntent = {
|
|
1148
1148
|
...reviewFinalizationIntent,
|
|
1149
|
-
state: '
|
|
1149
|
+
state: 'consumption-started',
|
|
1150
1150
|
phase: 'task-event.completed',
|
|
1151
1151
|
updatedAt: Date.now()
|
|
1152
1152
|
};
|
|
@@ -1183,7 +1183,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1183
1183
|
}
|
|
1184
1184
|
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
|
|
1185
1185
|
try {
|
|
1186
|
-
if (reviewFinalizationIntent.state === '
|
|
1186
|
+
if (reviewFinalizationIntent.state === 'consumption-started') {
|
|
1187
1187
|
const consumed = { ...reviewFinalizationIntent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
1188
1188
|
writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
|
|
1189
1189
|
}
|
|
@@ -327,10 +327,26 @@ function cleanupConsumedRecoveryFiles(repoRoot: string, task: TaskRecord | undef
|
|
|
327
327
|
let entries: fs.Dirent[];
|
|
328
328
|
try { entries = fs.readdirSync(recovery, { withFileTypes: true }); } catch { return false; }
|
|
329
329
|
for (const entry of entries) {
|
|
330
|
-
if (entry.isSymbolicLink()
|
|
330
|
+
if (entry.isSymbolicLink()) return false;
|
|
331
|
+
if (entry.isFile()) {
|
|
332
|
+
if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!entry.isDirectory() || entry.name !== 'generations') return false;
|
|
336
|
+
let generations: fs.Dirent[];
|
|
337
|
+
try { generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true }); } catch { return false; }
|
|
338
|
+
if (generations.some((generation) => generation.isSymbolicLink()
|
|
339
|
+
|| !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name))) return false;
|
|
331
340
|
}
|
|
332
341
|
try {
|
|
333
|
-
for (const entry of entries)
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
const target = path.join(recovery, entry.name);
|
|
344
|
+
if (entry.isFile()) fs.unlinkSync(target);
|
|
345
|
+
else {
|
|
346
|
+
for (const generation of fs.readdirSync(target, { withFileTypes: true })) fs.unlinkSync(path.join(target, generation.name));
|
|
347
|
+
fs.rmdirSync(target);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
334
350
|
fs.rmdirSync(recovery);
|
|
335
351
|
const recoveryRoot = path.dirname(recovery);
|
|
336
352
|
if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
|
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
beginArtifactRecovery,
|
|
20
20
|
commitArtifactRecovery,
|
|
21
21
|
consumeArtifactRecovery,
|
|
22
|
+
prepareArtifactRecoveryFinal,
|
|
22
23
|
prepareArtifactRecoveryCommit,
|
|
23
24
|
recordArtifactRecoveryPassed,
|
|
25
|
+
reconcileArtifactRecovery,
|
|
24
26
|
recoveryContextFromIntent,
|
|
25
27
|
stageArtifactCandidate
|
|
26
28
|
} from './artifact-recovery.ts';
|
|
@@ -265,7 +267,8 @@ function tupleFor(
|
|
|
265
267
|
function prepareLocalArtifact(
|
|
266
268
|
request: LocalArtifactFinalizationRequest,
|
|
267
269
|
candidate?: string,
|
|
268
|
-
authority?: LifecycleRecoveryAttestationV1
|
|
270
|
+
authority?: LifecycleRecoveryAttestationV1,
|
|
271
|
+
preflightOnly = false
|
|
269
272
|
): LocalArtifactPreparation {
|
|
270
273
|
const failed = (code: string, message: string, extra: Partial<LocalArtifactFinalizationResult> = {}): LocalArtifactPreparation => ({
|
|
271
274
|
result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
|
|
@@ -281,12 +284,14 @@ function prepareLocalArtifact(
|
|
|
281
284
|
let taskContent: string;
|
|
282
285
|
let content: string;
|
|
283
286
|
let recovery: ArtifactRecoveryContext | undefined;
|
|
287
|
+
let recoveryState: ArtifactRecoveryIntent['state'] | undefined;
|
|
284
288
|
try {
|
|
285
289
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
286
290
|
if (request.recoveryId) {
|
|
287
291
|
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
288
292
|
if (!intent || intent.recoveryOperationId !== request.recoveryId) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'recovery id does not match the artifact journal');
|
|
289
|
-
if (!['awaiting-recovery', '
|
|
293
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
|
|
294
|
+
recoveryState = intent.state;
|
|
290
295
|
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
291
296
|
content = fs.existsSync(recovery.stagingPath)
|
|
292
297
|
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
@@ -315,26 +320,66 @@ function prepareLocalArtifact(
|
|
|
315
320
|
|
|
316
321
|
if (request.recoveryId) {
|
|
317
322
|
if (!validation.ok) return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
323
|
+
if (preflightOnly && recovery && recoveryState === 'awaiting-preflight-recovery') {
|
|
324
|
+
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
325
|
+
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
326
|
+
}
|
|
318
327
|
return {
|
|
319
|
-
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(recovery!) },
|
|
328
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(recovery!) },
|
|
320
329
|
content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
321
330
|
};
|
|
322
331
|
}
|
|
323
332
|
|
|
324
333
|
const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
325
334
|
if (existing?.state === 'passed' || existing?.state === 'consumed') {
|
|
326
|
-
if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
|
|
335
|
+
if (existing.state === 'consumed' && (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest)) {
|
|
327
336
|
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
328
337
|
}
|
|
338
|
+
if (existing.finalArtifactSha256 === artifactSha256 && existing.finalSemanticDigest === validation.semanticDigest) {
|
|
339
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
340
|
+
return {
|
|
341
|
+
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
|
|
342
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (existing && ['preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready'].includes(existing.state)) {
|
|
329
348
|
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
330
349
|
return {
|
|
331
|
-
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
|
|
350
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
|
|
351
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (existing?.state === 'commit-started') {
|
|
355
|
+
if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
|
|
356
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match the interrupted recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
357
|
+
}
|
|
358
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
359
|
+
const reconciled = reconcileArtifactRecovery(context, {
|
|
360
|
+
lockAlreadyHeld: request.lockAlreadyHeld,
|
|
361
|
+
validateFinal: () => validation.semanticDigest === existing.finalSemanticDigest
|
|
362
|
+
});
|
|
363
|
+
if (reconciled.status !== 'passed') {
|
|
364
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `interrupted recovery could not be reconciled: ${reconciled.status}`), content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
|
|
332
368
|
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
333
369
|
};
|
|
334
370
|
}
|
|
335
371
|
|
|
336
372
|
try {
|
|
337
373
|
if (validation.ok) {
|
|
374
|
+
if (preflightOnly) {
|
|
375
|
+
const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
|
|
376
|
+
repoRoot: resolved.repoRoot, taskDir: resolved.taskDir,
|
|
377
|
+
...(authority ? { recoveryId: authority.operationId } : {}), lockAlreadyHeld: request.lockAlreadyHeld
|
|
378
|
+
});
|
|
379
|
+
const staged = stageArtifactCandidate(context, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
380
|
+
prepareArtifactRecoveryCommit(context, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
381
|
+
return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
382
|
+
}
|
|
338
383
|
const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
|
|
339
384
|
repoRoot: resolved.repoRoot,
|
|
340
385
|
taskDir: resolved.taskDir,
|
|
@@ -344,7 +389,7 @@ function prepareLocalArtifact(
|
|
|
344
389
|
lockAlreadyHeld: request.lockAlreadyHeld
|
|
345
390
|
});
|
|
346
391
|
recovery = context;
|
|
347
|
-
return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
392
|
+
return { result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
348
393
|
}
|
|
349
394
|
const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
|
|
350
395
|
repoRoot: resolved.repoRoot,
|
|
@@ -362,7 +407,10 @@ function prepareLocalArtifact(
|
|
|
362
407
|
}
|
|
363
408
|
}
|
|
364
409
|
|
|
365
|
-
function commitLocalArtifactProvenance(
|
|
410
|
+
function commitLocalArtifactProvenance(
|
|
411
|
+
prepared: LocalArtifactPreparation,
|
|
412
|
+
options: Readonly<{ afterPublish?: () => void }> = {}
|
|
413
|
+
): LocalArtifactFinalizationResult {
|
|
366
414
|
if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery) return prepared.result;
|
|
367
415
|
if (prepared.result.status === 'failed') return prepared.result;
|
|
368
416
|
try {
|
|
@@ -372,26 +420,42 @@ function commitLocalArtifactProvenance(prepared: LocalArtifactPreparation): Loca
|
|
|
372
420
|
if (intent.state === 'passed' || intent.state === 'consumed') {
|
|
373
421
|
return {
|
|
374
422
|
...prepared.result,
|
|
423
|
+
changed: false,
|
|
375
424
|
artifactSha256: intent.finalArtifactSha256,
|
|
376
425
|
semanticDigest: intent.finalSemanticDigest,
|
|
377
426
|
error: null
|
|
378
427
|
};
|
|
379
|
-
} else if (intent.state === '
|
|
380
|
-
|
|
381
|
-
|
|
428
|
+
} else if (intent.state === 'commit-started') {
|
|
429
|
+
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
430
|
+
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
431
|
+
afterPublish: options.afterPublish
|
|
432
|
+
});
|
|
433
|
+
committed = reconciled.intent;
|
|
434
|
+
} else if (intent.state === 'awaiting-preflight-recovery') {
|
|
382
435
|
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
383
436
|
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
384
437
|
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
385
438
|
}
|
|
386
|
-
if (
|
|
439
|
+
if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
440
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
441
|
+
}
|
|
442
|
+
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
443
|
+
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
444
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
445
|
+
} else if (intent.state === 'full-finalizer-ready') {
|
|
446
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
447
|
+
}
|
|
448
|
+
if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
387
449
|
return {
|
|
388
450
|
...prepared.result,
|
|
389
451
|
status: 'passed',
|
|
452
|
+
changed: false,
|
|
390
453
|
artifactSha256: committed.finalArtifactSha256,
|
|
391
454
|
semanticDigest: committed.finalSemanticDigest,
|
|
392
455
|
error: null
|
|
393
456
|
};
|
|
394
457
|
} catch (error) {
|
|
458
|
+
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
|
|
395
459
|
return { ...prepared.result, status: 'failed', error: {
|
|
396
460
|
code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
|
|
397
461
|
} };
|
|
@@ -417,10 +481,20 @@ function finalizeLocalArtifact(
|
|
|
417
481
|
return result;
|
|
418
482
|
}
|
|
419
483
|
|
|
484
|
+
/** Validate and seal an immutable preflight generation without publishing the formal artifact. */
|
|
485
|
+
function preflightLocalArtifact(
|
|
486
|
+
request: LocalArtifactFinalizationRequest,
|
|
487
|
+
authority?: LifecycleRecoveryAttestationV1
|
|
488
|
+
): LocalArtifactFinalizationResult {
|
|
489
|
+
const prepared = prepareLocalArtifact(request, undefined, authority, true);
|
|
490
|
+
return prepared.result;
|
|
491
|
+
}
|
|
492
|
+
|
|
420
493
|
export {
|
|
421
494
|
LOCAL_ARTIFACT_REQUIRED_SECTIONS,
|
|
422
495
|
consumeLocalArtifactFinalizationIntent,
|
|
423
496
|
finalizeLocalArtifact,
|
|
497
|
+
preflightLocalArtifact,
|
|
424
498
|
prepareLocalArtifact,
|
|
425
499
|
commitLocalArtifactProvenance,
|
|
426
500
|
readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent,
|
|
@@ -3,7 +3,7 @@ import type { ReviewFinalizationRequest } from './review-finalization.ts';
|
|
|
3
3
|
export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest & {
|
|
4
4
|
overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
|
|
5
5
|
} {
|
|
6
|
-
if (!args[0] || args[1]
|
|
6
|
+
if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? '')) throw new Error('task ref and a supported review operation are required');
|
|
7
7
|
const values: Record<string, string | boolean> = {};
|
|
8
8
|
const flags = {
|
|
9
9
|
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
|