@fitlab-ai/agent-infra 0.9.15 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +0 -1
- package/dist/lib/internal/task-review.js +4 -4
- package/dist/lib/platform/issue-comments.js +16 -10
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/verification-sync.js +6 -2
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/workflow-executor.js +4 -14
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/task/artifact-command.js +6 -28
- package/dist/lib/task/artifact-operations.js +16 -230
- package/dist/lib/task/artifact-recovery.js +430 -0
- package/dist/lib/task/artifact-repair-intent.js +73 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +38 -7
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +164 -22
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +66 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +148 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +1 -0
- package/dist/lib/task/review-finalization.js +104 -77
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +0 -1
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +4 -3
- package/lib/internal/task-operation-registry.ts +0 -1
- package/lib/internal/task-review.ts +4 -4
- package/lib/platform/issue-comments.ts +18 -10
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/verification-sync.ts +6 -2
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/protocol.ts +5 -0
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/workflow-executor.ts +4 -14
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/task/artifact-command.ts +8 -27
- package/lib/task/artifact-operations.ts +21 -251
- package/lib/task/artifact-recovery.ts +519 -0
- package/lib/task/artifact-repair-intent.ts +99 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +47 -14
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +187 -23
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +50 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +176 -132
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +1 -0
- package/lib/task/review-finalization.ts +106 -87
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +0 -1
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +4 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
package/lib/task/lifecycle.ts
CHANGED
|
@@ -15,9 +15,11 @@ import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
|
15
15
|
import type { TaskFileSystem, TaskMutation, TaskOperationSummary, TaskWriteMetadata } from './write.ts';
|
|
16
16
|
import { validateCurrentTaskContract } from './current-contract.ts';
|
|
17
17
|
import { isValidAgentInfraVersion } from '../version.ts';
|
|
18
|
+
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
19
|
+
import { resourceIdentityEquals } from '../platform/resource-identity.ts';
|
|
18
20
|
|
|
19
21
|
const lifecycleIntentCatalog = [
|
|
20
|
-
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
|
|
22
|
+
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'
|
|
21
23
|
] as const;
|
|
22
24
|
const lifecycleFailureCatalog = [
|
|
23
25
|
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
@@ -55,7 +57,8 @@ type TaskLifecycleRequest =
|
|
|
55
57
|
| { taskRef: string; intent: 'complete'; agent: string; dryRun?: boolean }
|
|
56
58
|
| { taskRef: string; intent: 'close-codescan'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
57
59
|
| { taskRef: string; intent: 'close-dependabot'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
58
|
-
| { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
|
|
60
|
+
| { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
|
|
61
|
+
| { taskRef: string; intent: 'recover-started'; agent: string; stage: 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code'; round: number; artifact: string; reason: string; dryRun?: boolean };
|
|
59
62
|
|
|
60
63
|
type HotState = 'active' | 'blocked' | 'completed';
|
|
61
64
|
type SourceState = HotState | 'staging';
|
|
@@ -206,6 +209,7 @@ function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { acti
|
|
|
206
209
|
if (request.intent === 'complete') return { action: 'Complete Task', note: 'Task moved to completed/' };
|
|
207
210
|
if (request.intent === 'close-codescan') return { action: 'Close Codescan', note: `Code Scanning alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
208
211
|
if (request.intent === 'close-dependabot') return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
212
|
+
if (request.intent === 'recover-started') return { action: 'Recover Started', note: request.reason };
|
|
209
213
|
return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
|
|
210
214
|
}
|
|
211
215
|
|
|
@@ -249,8 +253,10 @@ function validateRestoreStaging(request: Extract<TaskLifecycleRequest, { intent:
|
|
|
249
253
|
if (!fs.existsSync(taskFile)) return { code: 'LIFECYCLE_STAGING_INVALID', message: 'restore staging has no task.md' };
|
|
250
254
|
try {
|
|
251
255
|
const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(taskFile, 'utf8'));
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
const issueIdentity = taskIssueIdentity(frontmatter);
|
|
257
|
+
const requestedIssueIdentity = { kind: 'number' as const, value: request.issueNumber };
|
|
258
|
+
if (frontmatter.id !== taskId || !issueIdentity || !resourceIdentityEquals(issueIdentity, requestedIssueIdentity) || frontmatter.current_step === 'completed') {
|
|
259
|
+
return { code: 'LIFECYCLE_STAGING_IDENTITY_INVALID', message: 'restore task identity, platform issue identity, or current_step is invalid' };
|
|
254
260
|
}
|
|
255
261
|
} catch (error) {
|
|
256
262
|
return { code: 'LIFECYCLE_STAGING_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
@@ -410,6 +416,9 @@ function removeCopiedSource(source: string, target: string): void {
|
|
|
410
416
|
}
|
|
411
417
|
|
|
412
418
|
function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLifecycleOptions = {}): TaskLifecycleResult {
|
|
419
|
+
if (requestInput.intent === 'recover-started') {
|
|
420
|
+
return failed(requestInput, { code: 'LIFECYCLE_RECOVERY_REQUIRES_AUTHORITY', message: 'recover-started must be dispatched through task control authority' });
|
|
421
|
+
}
|
|
413
422
|
const normalized = normalizedRequest(requestInput);
|
|
414
423
|
if ('code' in normalized) return failed(requestInput, normalized);
|
|
415
424
|
const request = normalized;
|
|
@@ -5,6 +5,7 @@ import { scanVisibleMarkdown } from './markdown.ts';
|
|
|
5
5
|
import { parseArtifactName } from './artifact-name.ts';
|
|
6
6
|
import {
|
|
7
7
|
hasOpenArtifactRound,
|
|
8
|
+
parseCodePlanInputReference,
|
|
8
9
|
validateArtifactPublication,
|
|
9
10
|
validateCompletedArtifact
|
|
10
11
|
} from './artifact-lifecycle.ts';
|
|
@@ -12,8 +13,18 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
12
13
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
13
14
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
|
|
14
15
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
15
|
-
import {
|
|
16
|
-
import type {
|
|
16
|
+
import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
17
|
+
import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
18
|
+
import {
|
|
19
|
+
beginArtifactRecovery,
|
|
20
|
+
commitArtifactRecovery,
|
|
21
|
+
consumeArtifactRecovery,
|
|
22
|
+
prepareArtifactRecoveryCommit,
|
|
23
|
+
recordArtifactRecoveryPassed,
|
|
24
|
+
recoveryContextFromIntent,
|
|
25
|
+
stageArtifactCandidate
|
|
26
|
+
} from './artifact-recovery.ts';
|
|
27
|
+
import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
|
|
17
28
|
import {
|
|
18
29
|
consumeLifecycleRecoveryAttestation,
|
|
19
30
|
lifecycleRecoveryAttestationDigest,
|
|
@@ -38,18 +49,14 @@ type LocalArtifactDiagnosticCode =
|
|
|
38
49
|
| 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
39
50
|
| 'LOCAL_STRUCTURAL_INVALID'
|
|
40
51
|
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
41
|
-
| '
|
|
42
|
-
| '
|
|
52
|
+
| 'LOCAL_RECOVERY_PROVENANCE_CONFLICT'
|
|
53
|
+
| 'LOCAL_RECOVERY_BASELINE_MISMATCH'
|
|
43
54
|
| 'LOCAL_QUALIFICATION_AUDIT_INVALID';
|
|
44
55
|
|
|
45
56
|
type LocalArtifactDiagnostic = {
|
|
46
57
|
code: LocalArtifactDiagnosticCode;
|
|
47
58
|
message: string;
|
|
48
|
-
repairable: boolean;
|
|
49
59
|
line: number | null;
|
|
50
|
-
from?: string;
|
|
51
|
-
to?: string;
|
|
52
|
-
operation?: 'replace-line';
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
type LocalArtifactValidationOptions = {
|
|
@@ -63,7 +70,6 @@ type LocalArtifactValidationResult = {
|
|
|
63
70
|
ok: boolean;
|
|
64
71
|
family: LocalArtifactFamily;
|
|
65
72
|
semanticDigest: string;
|
|
66
|
-
repairable: boolean;
|
|
67
73
|
diagnostics: readonly LocalArtifactDiagnostic[];
|
|
68
74
|
};
|
|
69
75
|
|
|
@@ -73,23 +79,32 @@ type LocalArtifactFinalizationRequest = {
|
|
|
73
79
|
artifact: string;
|
|
74
80
|
repoRoot?: string;
|
|
75
81
|
requiredSections?: readonly string[];
|
|
82
|
+
recoveryId?: string;
|
|
83
|
+
lockAlreadyHeld?: boolean;
|
|
76
84
|
};
|
|
77
85
|
|
|
86
|
+
type LocalArtifactRecoveryInfo = Readonly<{
|
|
87
|
+
recoveryId: string;
|
|
88
|
+
candidatePath: string;
|
|
89
|
+
baselineSha256: string;
|
|
90
|
+
baselineSemanticDigest: string;
|
|
91
|
+
}>;
|
|
92
|
+
|
|
78
93
|
type LocalArtifactFinalizationResult = {
|
|
79
94
|
status: 'passed' | 'failed';
|
|
80
|
-
changed:
|
|
95
|
+
changed: boolean;
|
|
81
96
|
taskId: string | null;
|
|
82
97
|
taskDir: string | null;
|
|
83
98
|
family: LocalArtifactFamily;
|
|
84
99
|
artifact: string;
|
|
85
100
|
artifactSha256: string | null;
|
|
86
101
|
semanticDigest: string | null;
|
|
87
|
-
repairable: boolean;
|
|
88
102
|
diagnostics: readonly LocalArtifactDiagnostic[];
|
|
103
|
+
recovery?: LocalArtifactRecoveryInfo;
|
|
89
104
|
error: { code: string; message: string } | null;
|
|
90
105
|
};
|
|
91
106
|
|
|
92
|
-
type LocalArtifactFinalizationIntent =
|
|
107
|
+
type LocalArtifactFinalizationIntent = ArtifactRecoveryIntent;
|
|
93
108
|
|
|
94
109
|
function authorityDigest(authority: LifecycleRecoveryAttestationV1): string {
|
|
95
110
|
return lifecycleRecoveryAttestationDigest(authority);
|
|
@@ -113,18 +128,27 @@ function validFinalizerAuthority(
|
|
|
113
128
|
&& authority.operationId.length > 0;
|
|
114
129
|
}
|
|
115
130
|
|
|
131
|
+
function recoveryInfo(context: ArtifactRecoveryContext): LocalArtifactRecoveryInfo {
|
|
132
|
+
return {
|
|
133
|
+
recoveryId: context.recoveryId,
|
|
134
|
+
candidatePath: context.stagingPath,
|
|
135
|
+
baselineSha256: context.baselineSha256,
|
|
136
|
+
baselineSemanticDigest: context.baselineSemanticDigest
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
function consumeLocalArtifactFinalizationIntent(
|
|
117
141
|
repoRoot: string,
|
|
118
|
-
intent: LocalArtifactFinalizationIntent
|
|
142
|
+
intent: LocalArtifactFinalizationIntent,
|
|
143
|
+
options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
|
|
119
144
|
): LocalArtifactFinalizationIntent {
|
|
120
145
|
if (intent.state === 'consumed') return intent;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return consumed;
|
|
146
|
+
const resolved = resolveTaskRef(intent.taskId, { repoRoot });
|
|
147
|
+
if (!resolved.ok) throw new Error(resolved.message);
|
|
148
|
+
return consumeArtifactRecovery(recoveryContextFromIntent(repoRoot, resolved.taskDir, intent), options);
|
|
125
149
|
}
|
|
126
150
|
|
|
127
|
-
function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode
|
|
151
|
+
function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
|
|
128
152
|
const map: Record<string, LocalArtifactDiagnosticCode> = {
|
|
129
153
|
ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
|
|
130
154
|
ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
|
|
@@ -134,20 +158,8 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode | null {
|
|
|
134
158
|
return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
|
|
135
159
|
}
|
|
136
160
|
|
|
137
|
-
function localDiagnostic(
|
|
138
|
-
|
|
139
|
-
): LocalArtifactDiagnostic | null {
|
|
140
|
-
const code = localDiagnosticCode(item.code);
|
|
141
|
-
if (!code) return null;
|
|
142
|
-
return {
|
|
143
|
-
code,
|
|
144
|
-
message: item.message,
|
|
145
|
-
repairable: item.repairable,
|
|
146
|
-
line: item.line,
|
|
147
|
-
...(item.operation?.kind === 'replace-line'
|
|
148
|
-
? { from: item.operation.from, to: item.operation.to, operation: 'replace-line' as const }
|
|
149
|
-
: {})
|
|
150
|
-
};
|
|
161
|
+
function localDiagnostic(item: { code: string; message: string; line: number | null }): LocalArtifactDiagnostic {
|
|
162
|
+
return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
function isStatusPattern(pattern: string): boolean {
|
|
@@ -159,14 +171,10 @@ function validateLocalArtifact(
|
|
|
159
171
|
options: LocalArtifactValidationOptions
|
|
160
172
|
): LocalArtifactValidationResult {
|
|
161
173
|
const schema = getArtifactSchema(options.family)!;
|
|
162
|
-
const patterns = schema.requiredPatterns;
|
|
163
174
|
const diagnostics: LocalArtifactDiagnostic[] = [];
|
|
164
175
|
const scanned = scanVisibleMarkdown(content);
|
|
165
176
|
const structure = inspectArtifactStructure(content, schema);
|
|
166
|
-
for (const item of structure.diagnostics)
|
|
167
|
-
const mapped = localDiagnostic(item);
|
|
168
|
-
if (mapped) diagnostics.push(mapped);
|
|
169
|
-
}
|
|
177
|
+
for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
|
|
170
178
|
|
|
171
179
|
const statusSection = schema.sections.find((section) => section.id === 'state-check');
|
|
172
180
|
if (statusSection) {
|
|
@@ -178,9 +186,9 @@ function validateLocalArtifact(
|
|
|
178
186
|
if (statusHeading) {
|
|
179
187
|
const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
|
|
180
188
|
const body = content.slice(statusHeading.end, next?.start ?? content.length);
|
|
181
|
-
for (const pattern of
|
|
189
|
+
for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
|
|
182
190
|
if (!new RegExp(pattern, 'm').test(body)) {
|
|
183
|
-
diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`,
|
|
191
|
+
diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, line: content.slice(0, statusHeading.start).split('\n').length });
|
|
184
192
|
}
|
|
185
193
|
}
|
|
186
194
|
}
|
|
@@ -188,40 +196,28 @@ function validateLocalArtifact(
|
|
|
188
196
|
|
|
189
197
|
const patternInspection = inspectArtifactPatterns(content, {
|
|
190
198
|
...schema,
|
|
191
|
-
requiredPatterns:
|
|
199
|
+
requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
|
|
192
200
|
});
|
|
193
|
-
for (const item of patternInspection.diagnostics) {
|
|
194
|
-
diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, repairable: false, line: null });
|
|
195
|
-
}
|
|
201
|
+
for (const item of patternInspection.diagnostics) diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
|
|
196
202
|
|
|
197
203
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
198
|
-
if (!decisionDetails.ok) {
|
|
199
|
-
diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, repairable: false, line: null });
|
|
200
|
-
}
|
|
204
|
+
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
201
205
|
|
|
202
206
|
if (options.taskContent !== undefined) {
|
|
203
207
|
const expected = expectedQualificationRelations(options.taskContent, options.family);
|
|
204
|
-
if (!expected.ok) {
|
|
205
|
-
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, repairable: false, line: null });
|
|
206
|
-
}
|
|
208
|
+
if (!expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
207
209
|
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
208
|
-
family: options.family
|
|
210
|
+
family: options.family,
|
|
209
211
|
artifact: options.artifact,
|
|
210
212
|
expectedUpstreamRelations: expected.ok ? expected.relations : undefined
|
|
211
213
|
});
|
|
212
|
-
if (!qualification.ok) {
|
|
213
|
-
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, repairable: false, line: null });
|
|
214
|
-
}
|
|
214
|
+
if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
const repairable = structure.repair !== null && diagnostics.length === 1 && diagnostics[0]?.repairable === true;
|
|
218
|
-
const semanticDigestValue = canonicalSemanticDigest(content, structure.repair);
|
|
219
|
-
|
|
220
217
|
return {
|
|
221
218
|
ok: diagnostics.length === 0,
|
|
222
219
|
family: options.family,
|
|
223
|
-
semanticDigest:
|
|
224
|
-
repairable,
|
|
220
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
225
221
|
diagnostics
|
|
226
222
|
};
|
|
227
223
|
}
|
|
@@ -236,7 +232,7 @@ function failedFinalization(
|
|
|
236
232
|
taskId: null, taskDir: null,
|
|
237
233
|
family: request.family, artifact: request.artifact,
|
|
238
234
|
artifactSha256: null, semanticDigest: null,
|
|
239
|
-
|
|
235
|
+
diagnostics: [], error, ...extra
|
|
240
236
|
};
|
|
241
237
|
}
|
|
242
238
|
|
|
@@ -244,114 +240,160 @@ type LocalArtifactPreparation = Readonly<{
|
|
|
244
240
|
result: LocalArtifactFinalizationResult;
|
|
245
241
|
content: string;
|
|
246
242
|
repoRoot?: string;
|
|
247
|
-
|
|
248
|
-
|
|
243
|
+
lockAlreadyHeld?: boolean;
|
|
244
|
+
recovery?: ArtifactRecoveryContext;
|
|
245
|
+
authority?: LifecycleRecoveryAttestationV1;
|
|
249
246
|
}>;
|
|
250
247
|
|
|
251
|
-
|
|
248
|
+
function tupleFor(
|
|
249
|
+
request: LocalArtifactFinalizationRequest,
|
|
250
|
+
taskId: string,
|
|
251
|
+
authority?: LifecycleRecoveryAttestationV1
|
|
252
|
+
) {
|
|
253
|
+
const round = parseArtifactName(request.artifact)?.round ?? 0;
|
|
254
|
+
return {
|
|
255
|
+
taskId,
|
|
256
|
+
family: request.family,
|
|
257
|
+
artifact: request.artifact,
|
|
258
|
+
round,
|
|
259
|
+
requestId: authority?.lifecycleRequestId ?? `local-finalize:${taskId}:${request.family}:${round}`,
|
|
260
|
+
phase: authority?.phase ?? 'artifact.finalize-local' as const,
|
|
261
|
+
authorityDigest: authority ? authorityDigest(authority) : null
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
252
265
|
function prepareLocalArtifact(
|
|
253
266
|
request: LocalArtifactFinalizationRequest,
|
|
254
267
|
candidate?: string,
|
|
255
268
|
authority?: LifecycleRecoveryAttestationV1
|
|
256
269
|
): LocalArtifactPreparation {
|
|
257
|
-
const failed = (code: string, message: string): LocalArtifactPreparation => ({
|
|
258
|
-
result: failedFinalization(request, { code, message }), content: candidate ?? ''
|
|
270
|
+
const failed = (code: string, message: string, extra: Partial<LocalArtifactFinalizationResult> = {}): LocalArtifactPreparation => ({
|
|
271
|
+
result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
|
|
259
272
|
});
|
|
260
273
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
|
|
261
274
|
if (!resolved.ok) return failed(resolved.code, resolved.message);
|
|
262
275
|
const parsed = parseArtifactName(request.artifact);
|
|
263
276
|
if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
|
|
264
|
-
|
|
277
|
+
if (authority && !validFinalizerAuthority(authority, request, resolved.taskId)) {
|
|
278
|
+
return failed('LOCAL_EXECUTION_AUTHORITY_INVALID', 'finalizer authority does not match the artifact tuple');
|
|
279
|
+
}
|
|
280
|
+
|
|
265
281
|
let taskContent: string;
|
|
282
|
+
let content: string;
|
|
283
|
+
let recovery: ArtifactRecoveryContext | undefined;
|
|
266
284
|
try {
|
|
267
285
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
268
|
-
if (
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
if (request.recoveryId) {
|
|
287
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
288
|
+
if (!intent || intent.recoveryOperationId !== request.recoveryId) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'recovery id does not match the artifact journal');
|
|
289
|
+
if (!['awaiting-recovery', 'finalize-ready', 'commit-started'].includes(intent.state)) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
|
|
290
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
291
|
+
content = fs.existsSync(recovery.stagingPath)
|
|
292
|
+
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
293
|
+
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
275
294
|
} else {
|
|
276
295
|
const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
|
|
277
296
|
if (!validated.ok) return failed(validated.error.code, validated.error.message);
|
|
278
|
-
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
297
|
+
content = candidate ?? fs.readFileSync(validated.artifact.path, 'utf8');
|
|
279
298
|
}
|
|
280
299
|
} catch (error) { return failed('ARTIFACT_NOT_READABLE', String(error)); }
|
|
300
|
+
|
|
301
|
+
if (resolved.state !== 'active' || !hasOpenArtifactRound(taskContent, request.family, parsed.round)) {
|
|
302
|
+
return failed('ARTIFACT_IDENTITY_INVALID', 'candidate must match one open started round in an active task');
|
|
303
|
+
}
|
|
281
304
|
const validation = validateLocalArtifact(content, {
|
|
282
305
|
family: request.family, requiredSections: request.requiredSections, taskContent, artifact: request.artifact
|
|
283
306
|
});
|
|
284
307
|
const artifactSha256 = sha256Content(content);
|
|
285
|
-
const { semanticDigest, repairable, diagnostics } = validation;
|
|
286
308
|
const result = failedFinalization(request, {
|
|
287
|
-
code: 'LOCAL_ARTIFACT_INVALID',
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
309
|
+
code: 'LOCAL_ARTIFACT_INVALID',
|
|
310
|
+
message: validation.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
311
|
+
}, {
|
|
312
|
+
taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256,
|
|
313
|
+
semanticDigest: validation.semanticDigest, diagnostics: validation.diagnostics
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (request.recoveryId) {
|
|
317
|
+
if (!validation.ok) return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
291
318
|
return {
|
|
292
|
-
...
|
|
293
|
-
|
|
294
|
-
code: 'LOCAL_EXECUTION_AUTHORITY_INVALID',
|
|
295
|
-
message: 'finalizer authority does not match the artifact tuple'
|
|
296
|
-
}, diagnostics: [] }
|
|
319
|
+
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(recovery!) },
|
|
320
|
+
content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
297
321
|
};
|
|
298
322
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
try { intent = readArtifactRepairIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact); }
|
|
305
|
-
catch (error) {
|
|
306
|
-
return { ...preparedBase, result: { ...result, repairable: false, diagnostics: [],
|
|
307
|
-
error: { code: 'LOCAL_FINALIZATION_INTENT_INVALID', message: String(error) } } };
|
|
308
|
-
}
|
|
309
|
-
const prepared = { ...preparedBase, expectedIntent: intent };
|
|
310
|
-
if (intent && intent.recoveryOperationId !== null
|
|
311
|
-
&& (!authority || intent.recoveryOperationId !== authority.operationId)) {
|
|
312
|
-
return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'local repair provenance is owned by another recovery operation');
|
|
313
|
-
}
|
|
314
|
-
if (repairable) {
|
|
315
|
-
if (intent && (intent.state !== 'awaiting-repair' || intent.baselineSemanticDigest !== semanticDigest)) {
|
|
316
|
-
return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'a different local repair baseline is already recorded for this artifact');
|
|
317
|
-
}
|
|
318
|
-
if (intent && !authority) return prepared;
|
|
319
|
-
} else {
|
|
320
|
-
if (!validation.ok) return prepared;
|
|
321
|
-
if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== semanticDigest) {
|
|
322
|
-
return reject('LOCAL_REPAIR_BASELINE_MISMATCH', 'the repaired artifact semantic digest does not match the recorded repair baseline');
|
|
323
|
+
|
|
324
|
+
const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
325
|
+
if (existing?.state === 'passed' || existing?.state === 'consumed') {
|
|
326
|
+
if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
|
|
327
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
323
328
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
329
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
330
|
+
return {
|
|
331
|
+
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
|
|
332
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
if (validation.ok) {
|
|
338
|
+
const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
|
|
339
|
+
repoRoot: resolved.repoRoot,
|
|
340
|
+
taskDir: resolved.taskDir,
|
|
341
|
+
expectedFinalSha256: artifactSha256,
|
|
342
|
+
expectedFinalSemanticDigest: validation.semanticDigest,
|
|
343
|
+
...(authority ? { recoveryId: authority.operationId } : {}),
|
|
344
|
+
lockAlreadyHeld: request.lockAlreadyHeld
|
|
345
|
+
});
|
|
346
|
+
recovery = context;
|
|
347
|
+
return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
327
348
|
}
|
|
328
|
-
|
|
329
|
-
|
|
349
|
+
const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
|
|
350
|
+
repoRoot: resolved.repoRoot,
|
|
351
|
+
taskDir: resolved.taskDir,
|
|
352
|
+
...(authority ? { recoveryId: authority.operationId } : {}),
|
|
353
|
+
lockAlreadyHeld: request.lockAlreadyHeld
|
|
354
|
+
});
|
|
355
|
+
recovery = context;
|
|
356
|
+
return {
|
|
357
|
+
result: { ...result, recovery: recoveryInfo(context) },
|
|
358
|
+
content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
359
|
+
};
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', String(error)), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
330
362
|
}
|
|
331
|
-
const timestamp = Date.now();
|
|
332
|
-
return { ...prepared, provenance: {
|
|
333
|
-
version: 2, taskId: resolved.taskId, family: request.family, artifact: request.artifact,
|
|
334
|
-
state: repairable ? 'awaiting-repair' : 'passed',
|
|
335
|
-
baselineSemanticDigest: repairable ? semanticDigest : intent?.baselineSemanticDigest ?? null,
|
|
336
|
-
artifactSha256, semanticDigest,
|
|
337
|
-
recoveryOperationId: authority?.operationId ?? intent?.recoveryOperationId ?? null,
|
|
338
|
-
phase: authority?.phase ?? intent?.phase ?? null,
|
|
339
|
-
authorityDigest: authority ? authorityDigest(authority) : intent?.authorityDigest ?? null,
|
|
340
|
-
requestId: authority?.lifecycleRequestId ?? intent?.requestId ?? `local-finalize:${resolved.taskId}`,
|
|
341
|
-
createdAt: intent?.createdAt ?? timestamp,
|
|
342
|
-
updatedAt: timestamp
|
|
343
|
-
} };
|
|
344
363
|
}
|
|
345
364
|
|
|
346
365
|
function commitLocalArtifactProvenance(prepared: LocalArtifactPreparation): LocalArtifactFinalizationResult {
|
|
366
|
+
if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery) return prepared.result;
|
|
367
|
+
if (prepared.result.status === 'failed') return prepared.result;
|
|
347
368
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
369
|
+
const intent = readArtifactRecoveryIntent(prepared.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
|
|
370
|
+
if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
|
|
371
|
+
let committed;
|
|
372
|
+
if (intent.state === 'passed' || intent.state === 'consumed') {
|
|
373
|
+
return {
|
|
374
|
+
...prepared.result,
|
|
375
|
+
artifactSha256: intent.finalArtifactSha256,
|
|
376
|
+
semanticDigest: intent.finalSemanticDigest,
|
|
377
|
+
error: null
|
|
378
|
+
};
|
|
379
|
+
} else if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
|
|
380
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
381
|
+
} else {
|
|
382
|
+
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
383
|
+
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
384
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
350
385
|
}
|
|
351
|
-
|
|
386
|
+
if (committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
|
|
387
|
+
return {
|
|
388
|
+
...prepared.result,
|
|
389
|
+
status: 'passed',
|
|
390
|
+
artifactSha256: committed.finalArtifactSha256,
|
|
391
|
+
semanticDigest: committed.finalSemanticDigest,
|
|
392
|
+
error: null
|
|
393
|
+
};
|
|
352
394
|
} catch (error) {
|
|
353
395
|
return { ...prepared.result, status: 'failed', error: {
|
|
354
|
-
code: '
|
|
396
|
+
code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
|
|
355
397
|
} };
|
|
356
398
|
}
|
|
357
399
|
}
|
|
@@ -361,7 +403,8 @@ function finalizeLocalArtifact(
|
|
|
361
403
|
authority?: LifecycleRecoveryAttestationV1,
|
|
362
404
|
options: Readonly<{ deferLifecycleRecoveryConsumption?: boolean }> = {}
|
|
363
405
|
): LocalArtifactFinalizationResult {
|
|
364
|
-
const
|
|
406
|
+
const prepared = prepareLocalArtifact(request, undefined, authority);
|
|
407
|
+
const result = commitLocalArtifactProvenance(prepared);
|
|
365
408
|
if (authority && !options.deferLifecycleRecoveryConsumption && result.status === 'passed') {
|
|
366
409
|
try { consumeLifecycleRecoveryAttestation(authority); }
|
|
367
410
|
catch (error) {
|
|
@@ -380,7 +423,7 @@ export {
|
|
|
380
423
|
finalizeLocalArtifact,
|
|
381
424
|
prepareLocalArtifact,
|
|
382
425
|
commitLocalArtifactProvenance,
|
|
383
|
-
|
|
426
|
+
readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent,
|
|
384
427
|
canonicalSemanticDigest as semanticDigest,
|
|
385
428
|
sha256Content,
|
|
386
429
|
validateLocalArtifact
|
|
@@ -394,5 +437,6 @@ export type {
|
|
|
394
437
|
LocalArtifactFinalizationIntent,
|
|
395
438
|
LocalArtifactPreparation,
|
|
396
439
|
LocalArtifactValidationOptions,
|
|
397
|
-
LocalArtifactValidationResult
|
|
440
|
+
LocalArtifactValidationResult,
|
|
441
|
+
LocalArtifactRecoveryInfo
|
|
398
442
|
};
|
|
@@ -68,7 +68,11 @@ function manualValidationReceiptDigest(receipt: ManualValidationReceipt | Manual
|
|
|
68
68
|
|
|
69
69
|
function manualValidationFinalSummaryDigest(body: string): string {
|
|
70
70
|
const heading = /^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/mu.exec(body);
|
|
71
|
-
const
|
|
71
|
+
const afterHeading = heading ? body.slice(heading.index + heading[0].length) : '';
|
|
72
|
+
const nextSection = heading ? /^#{1,3}\s/mu.exec(afterHeading) : null;
|
|
73
|
+
const projection = heading
|
|
74
|
+
? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
|
|
75
|
+
: body;
|
|
72
76
|
const preimage = projection.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
|
|
73
77
|
return createHash('sha256').update(preimage, 'utf8').digest('hex');
|
|
74
78
|
}
|
|
@@ -11,6 +11,7 @@ import type { LedgerDocument } from './ledger.ts';
|
|
|
11
11
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
12
12
|
import {
|
|
13
13
|
activateDelegation,
|
|
14
|
+
abortActivatedDelegation,
|
|
14
15
|
abortPreparedDelegation,
|
|
15
16
|
completeDelegationStage,
|
|
16
17
|
consumeDelegation,
|
|
@@ -59,6 +60,7 @@ import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
|
59
60
|
import { invalidationBlocks } from './invalidation.ts';
|
|
60
61
|
import type { InvalidationDocument } from './invalidation.ts';
|
|
61
62
|
import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
|
|
63
|
+
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
62
64
|
|
|
63
65
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
64
66
|
type ModelPolicySource = Readonly<{
|
|
@@ -1444,6 +1446,68 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
1444
1446
|
}
|
|
1445
1447
|
}
|
|
1446
1448
|
|
|
1449
|
+
function recoverActivatedOrchestrationDelegationUnderLock(
|
|
1450
|
+
taskRef: string,
|
|
1451
|
+
event: Readonly<{
|
|
1452
|
+
receiptId: string;
|
|
1453
|
+
stage: DelegationStage;
|
|
1454
|
+
round: number;
|
|
1455
|
+
artifact: string;
|
|
1456
|
+
startedAgent: string;
|
|
1457
|
+
childId: string;
|
|
1458
|
+
stopRevision: number;
|
|
1459
|
+
consumer: string;
|
|
1460
|
+
consumedAt: string;
|
|
1461
|
+
}>,
|
|
1462
|
+
options: OrchestrationOptions = {}
|
|
1463
|
+
): OrchestrationResult {
|
|
1464
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1465
|
+
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1466
|
+
const run = readRun(resolved.taskDir, options);
|
|
1467
|
+
if (!run) return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
|
|
1468
|
+
const matches = [
|
|
1469
|
+
...(run.pendingDelegation ? [run.pendingDelegation] : []),
|
|
1470
|
+
...run.receipts.filter((receipt) => receipt.id === event.receiptId)
|
|
1471
|
+
];
|
|
1472
|
+
const receipt = matches.find((candidate) => candidate.id === event.receiptId);
|
|
1473
|
+
if (!receipt) return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
|
|
1474
|
+
if (
|
|
1475
|
+
receipt.taskId !== resolved.taskId
|
|
1476
|
+
|| receipt.stage !== event.stage
|
|
1477
|
+
|| receipt.round !== event.round
|
|
1478
|
+
|| receipt.artifact !== event.artifact
|
|
1479
|
+
|| receipt.childId !== event.childId
|
|
1480
|
+
|| normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)
|
|
1481
|
+
) {
|
|
1482
|
+
return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
|
|
1483
|
+
}
|
|
1484
|
+
if (receipt.status === 'aborted' && run.pendingDelegation === null) {
|
|
1485
|
+
return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
1486
|
+
}
|
|
1487
|
+
if (run.pendingDelegation?.id !== receipt.id || receipt.status !== 'activated') {
|
|
1488
|
+
return failed('ORCHESTRATION_RECOVERY_UNSAFE', 'recovery requires one matching activated pending delegation', resolved.taskId);
|
|
1489
|
+
}
|
|
1490
|
+
if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
|
|
1491
|
+
return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
|
|
1492
|
+
}
|
|
1493
|
+
const aborted = abortActivatedDelegation(receipt, {
|
|
1494
|
+
childId: event.childId,
|
|
1495
|
+
stopRevision: event.stopRevision,
|
|
1496
|
+
consumer: event.consumer,
|
|
1497
|
+
consumedAt: event.consumedAt
|
|
1498
|
+
});
|
|
1499
|
+
if (!aborted.ok) return failed(aborted.code, aborted.message, resolved.taskId);
|
|
1500
|
+
const updated = withUpdatedRun(run, {
|
|
1501
|
+
status: 'running',
|
|
1502
|
+
pause: null,
|
|
1503
|
+
nextStage: null,
|
|
1504
|
+
pendingDelegation: null,
|
|
1505
|
+
receipts: Object.freeze([...run.receipts, aborted.receipt])
|
|
1506
|
+
}, options.now);
|
|
1507
|
+
saveRun(resolved.taskDir, updated);
|
|
1508
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1447
1511
|
function completeOrchestrationStage(
|
|
1448
1512
|
taskRef: string,
|
|
1449
1513
|
event: Parameters<typeof completeDelegationStage>[1],
|
|
@@ -1598,6 +1662,7 @@ export {
|
|
|
1598
1662
|
readRun,
|
|
1599
1663
|
reconcileMatchingOrchestrationDelegation,
|
|
1600
1664
|
recoverPreparedOrchestrationDelegation,
|
|
1665
|
+
recoverActivatedOrchestrationDelegationUnderLock,
|
|
1601
1666
|
routeOrchestration,
|
|
1602
1667
|
sealMatchingOrchestrationDelegation,
|
|
1603
1668
|
sealMatchingOrchestrationDelegationWithHostEvidence,
|