@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
package/dist/lib/task/events.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
3
4
|
import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
|
|
4
|
-
import { buildArtifactLinkSection, inspectArtifactDirectory,
|
|
5
|
+
import { buildArtifactLinkSection, inspectArtifactDirectory, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
5
6
|
import { artifactName, parseArtifactName } from './artifact-name.js';
|
|
6
7
|
import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
|
|
7
8
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
8
9
|
import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
9
10
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
10
11
|
import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.js';
|
|
12
|
+
import { extractReviewBaseline, extractReviewedHead, extractReviewedSnapshotTree } from './review-fingerprint.js';
|
|
11
13
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
12
14
|
import { findSectionHeading } from './sections.js';
|
|
13
15
|
import { validateLifecycleExecution } from './lifecycle-execution.js';
|
|
@@ -20,6 +22,7 @@ import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artif
|
|
|
20
22
|
import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
|
|
21
23
|
import { buildLifecycleFacts, canStart } from './capabilities.js';
|
|
22
24
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
|
|
25
|
+
import { reconcileTaskInvalidation } from './invalidation-command.js';
|
|
23
26
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
|
|
24
27
|
import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.js';
|
|
25
28
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
@@ -36,6 +39,27 @@ const eventCatalog = [
|
|
|
36
39
|
'manual-validation.started', 'manual-validation.completed',
|
|
37
40
|
'validation-run.started', 'validation-run.completed'
|
|
38
41
|
];
|
|
42
|
+
function approvedCleanReviewedCommit(reviewContent, verdict, repoRoot) {
|
|
43
|
+
if (verdict !== 'approved')
|
|
44
|
+
return null;
|
|
45
|
+
const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
|
|
46
|
+
const reviewedTree = extractReviewedSnapshotTree(reviewContent);
|
|
47
|
+
if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const currentHead = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
51
|
+
if (currentHead !== reviewedHead)
|
|
52
|
+
return null;
|
|
53
|
+
const currentTree = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
|
|
54
|
+
if (currentTree !== reviewedTree)
|
|
55
|
+
return null;
|
|
56
|
+
execFileSync('git', ['-C', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
|
|
57
|
+
return reviewedHead;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
39
63
|
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
|
|
40
64
|
const SCHEMAS = {
|
|
41
65
|
'analyze.started': { optional: ['round'] },
|
|
@@ -334,7 +358,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
|
|
|
334
358
|
}
|
|
335
359
|
intent = reconciled.intent;
|
|
336
360
|
}
|
|
337
|
-
if (!['passed', 'consumed'].includes(intent.state)) {
|
|
361
|
+
if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
|
|
338
362
|
return {
|
|
339
363
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
340
364
|
message: `review finalizer provenance is incomplete for ${artifact}`
|
|
@@ -345,9 +369,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
|
|
|
345
369
|
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
346
370
|
}
|
|
347
371
|
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
348
|
-
const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
|
|
349
372
|
if (intent.round !== round
|
|
350
|
-
|| intent.requestId !== expectedRequestId
|
|
351
373
|
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
352
374
|
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
353
375
|
return {
|
|
@@ -355,7 +377,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
|
|
|
355
377
|
message: `review finalizer provenance does not match ${artifact}`
|
|
356
378
|
};
|
|
357
379
|
}
|
|
358
|
-
if (intent.state === 'passed') {
|
|
380
|
+
if (intent.state === 'passed' || intent.state === 'consumption-started') {
|
|
359
381
|
const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
|
|
360
382
|
writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
|
|
361
383
|
}
|
|
@@ -595,34 +617,22 @@ function reworkIntentMutationForCompletion(content, taskDir, family, artifact, t
|
|
|
595
617
|
}
|
|
596
618
|
function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
|
|
597
619
|
if (family === 'code') {
|
|
598
|
-
let content;
|
|
599
|
-
try {
|
|
600
|
-
content = fs.readFileSync(artifact.path, 'utf8');
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
604
|
-
}
|
|
605
|
-
const input = parseCodePlanInputReference(content);
|
|
606
|
-
if (!input)
|
|
607
|
-
return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
608
620
|
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
609
621
|
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
610
622
|
if (!startedInput || !startedSha256)
|
|
611
623
|
return { ok: false, message: 'code.started plan input context is missing' };
|
|
612
|
-
if (input.name !== startedInput)
|
|
613
|
-
return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
614
624
|
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
615
|
-
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !==
|
|
616
|
-
return { ok: false, message: `code input '${
|
|
625
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
|
|
626
|
+
return { ok: false, message: `code input '${startedInput}' is not the latest plan artifact` };
|
|
617
627
|
}
|
|
618
628
|
try {
|
|
619
629
|
const inputSha256 = sha256File(plan.latest.path);
|
|
620
630
|
if (inputSha256 !== startedSha256)
|
|
621
|
-
return { ok: false, message: `code input ${
|
|
631
|
+
return { ok: false, message: `code input ${startedInput} changed after code.started` };
|
|
622
632
|
return {
|
|
623
633
|
ok: true,
|
|
624
634
|
receipt: {
|
|
625
|
-
event: 'code.completed', output: artifact.name, input:
|
|
635
|
+
event: 'code.completed', output: artifact.name, input: startedInput,
|
|
626
636
|
inputSha256, completedAt
|
|
627
637
|
}
|
|
628
638
|
};
|
|
@@ -634,25 +644,13 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
|
|
|
634
644
|
if (!family.startsWith('review-'))
|
|
635
645
|
return null;
|
|
636
646
|
const expectedFamily = reviewInputFamily(family);
|
|
637
|
-
let content;
|
|
638
|
-
try {
|
|
639
|
-
content = fs.readFileSync(artifact.path, 'utf8');
|
|
640
|
-
}
|
|
641
|
-
catch (error) {
|
|
642
|
-
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
643
|
-
}
|
|
644
|
-
const input = parseReviewedInputReference(content, expectedFamily);
|
|
645
|
-
if (!input)
|
|
646
|
-
return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
647
647
|
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
648
648
|
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
649
649
|
if (!startedInput || !startedSha256)
|
|
650
650
|
return { ok: false, message: 'review started input context is missing' };
|
|
651
|
-
if (input.name !== startedInput)
|
|
652
|
-
return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
653
651
|
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
654
|
-
if (current.status !== 'ready' || !current.latest || current.latest.name !==
|
|
655
|
-
return { ok: false, message: `review input '${
|
|
652
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
|
|
653
|
+
return { ok: false, message: `review input '${startedInput}' is not the latest ${expectedFamily} artifact` };
|
|
656
654
|
}
|
|
657
655
|
const event = family === 'review-analysis'
|
|
658
656
|
? 'review-analysis.completed'
|
|
@@ -663,17 +661,17 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
|
|
|
663
661
|
throw new Error('review input is not a regular file');
|
|
664
662
|
const inputSha256 = sha256File(current.latest.path);
|
|
665
663
|
if (inputSha256 !== startedSha256)
|
|
666
|
-
return { ok: false, message: `review input ${
|
|
664
|
+
return { ok: false, message: `review input ${startedInput} changed after review.started` };
|
|
667
665
|
return {
|
|
668
666
|
ok: true,
|
|
669
667
|
receipt: {
|
|
670
|
-
event, output: artifact.name, input:
|
|
668
|
+
event, output: artifact.name, input: startedInput,
|
|
671
669
|
inputSha256, completedAt
|
|
672
670
|
}
|
|
673
671
|
};
|
|
674
672
|
}
|
|
675
673
|
catch (error) {
|
|
676
|
-
return { ok: false, message: `cannot hash review input ${
|
|
674
|
+
return { ok: false, message: `cannot hash review input ${startedInput}: ${error instanceof Error ? error.message : String(error)}` };
|
|
677
675
|
}
|
|
678
676
|
}
|
|
679
677
|
function applyTaskEventUnlocked(request, options = {}) {
|
|
@@ -693,6 +691,24 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
693
691
|
catch (error) {
|
|
694
692
|
return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
695
693
|
}
|
|
694
|
+
const initialParts = eventParts(request.event);
|
|
695
|
+
if (initialParts.phase === 'started' && !request.dryRun) {
|
|
696
|
+
const reconciled = reconcileTaskInvalidation(request.taskRef, { repoRoot: resolved.repoRoot });
|
|
697
|
+
if (reconciled.status === 'failed') {
|
|
698
|
+
return failed(request, {
|
|
699
|
+
code: 'EVENT_TRANSITION_INVALID',
|
|
700
|
+
message: `INVALIDATION_RECONCILE_FAILED: ${reconciled.error?.code ?? 'UNKNOWN'}: ${reconciled.error?.message ?? ''}`
|
|
701
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
702
|
+
}
|
|
703
|
+
if (reconciled.changed) {
|
|
704
|
+
try {
|
|
705
|
+
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
696
712
|
let frontmatter;
|
|
697
713
|
try {
|
|
698
714
|
frontmatter = parseTypedTaskFrontmatter(content);
|
|
@@ -706,7 +722,6 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
706
722
|
const rows = startedBackedRows(pairEntries(section.entries));
|
|
707
723
|
let normalized = request;
|
|
708
724
|
let artifactContext = null;
|
|
709
|
-
const initialParts = eventParts(request.event);
|
|
710
725
|
if (initialParts.phase === 'started') {
|
|
711
726
|
const openIdentity = openStartedIdentity(rows, initialParts.family);
|
|
712
727
|
if (openIdentity && 'conflict' in openIdentity)
|
|
@@ -935,20 +950,15 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
935
950
|
if (reconciled.status === 'passed' || reconciled.status === 'consumed')
|
|
936
951
|
reviewFinalizationIntent = reconciled.intent;
|
|
937
952
|
}
|
|
938
|
-
if (!reviewFinalizationIntent || !['passed', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
953
|
+
if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
939
954
|
return failed(normalized, {
|
|
940
955
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
941
956
|
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
942
957
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
943
958
|
}
|
|
944
|
-
const reviewStage = eventIdentity.family === 'review-analysis'
|
|
945
|
-
? 'analysis'
|
|
946
|
-
: eventIdentity.family === 'review-plan' ? 'plan' : 'code';
|
|
947
959
|
const actualSha256 = sha256File(completedArtifact.path);
|
|
948
960
|
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
949
|
-
const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
|
|
950
961
|
if (reviewFinalizationIntent.round !== normalized.round
|
|
951
|
-
|| reviewFinalizationIntent.requestId !== expectedRequestId
|
|
952
962
|
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
953
963
|
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
954
964
|
return failed(normalized, {
|
|
@@ -1060,6 +1070,13 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1060
1070
|
else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
1061
1071
|
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
1062
1072
|
}
|
|
1073
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
|
|
1074
|
+
const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot);
|
|
1075
|
+
if (reviewedCommit)
|
|
1076
|
+
frontmatterSet.last_reviewed_commit = reviewedCommit;
|
|
1077
|
+
else if (normalized.verdict === 'approved')
|
|
1078
|
+
frontmatterSet.last_reviewed_commit = '';
|
|
1079
|
+
}
|
|
1063
1080
|
if (eventIdentity.phase === 'completed')
|
|
1064
1081
|
frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
|
|
1065
1082
|
if (eventIdentity.phase === 'started' && normalized.implementationInput)
|
|
@@ -1106,7 +1123,7 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1106
1123
|
const expectedIntent = localFinalizationIntent;
|
|
1107
1124
|
localFinalizationIntent = {
|
|
1108
1125
|
...localFinalizationIntent,
|
|
1109
|
-
state: '
|
|
1126
|
+
state: 'consumption-started',
|
|
1110
1127
|
phase: 'task-event.completed',
|
|
1111
1128
|
authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
|
|
1112
1129
|
requestId: lifecycleAuthority.lifecycleRequestId,
|
|
@@ -1138,7 +1155,7 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1138
1155
|
const expectedIntent = reviewFinalizationIntent;
|
|
1139
1156
|
reviewFinalizationIntent = {
|
|
1140
1157
|
...reviewFinalizationIntent,
|
|
1141
|
-
state: '
|
|
1158
|
+
state: 'consumption-started',
|
|
1142
1159
|
phase: 'task-event.completed',
|
|
1143
1160
|
updatedAt: Date.now()
|
|
1144
1161
|
};
|
|
@@ -1179,7 +1196,7 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1179
1196
|
}
|
|
1180
1197
|
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
|
|
1181
1198
|
try {
|
|
1182
|
-
if (reviewFinalizationIntent.state === '
|
|
1199
|
+
if (reviewFinalizationIntent.state === 'consumption-started') {
|
|
1183
1200
|
const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
|
|
1184
1201
|
writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
|
|
1185
1202
|
}
|
|
@@ -1230,7 +1247,7 @@ function applyTaskEvent(request, options = {}) {
|
|
|
1230
1247
|
const parts = eventParts(request.event);
|
|
1231
1248
|
if (options.lockAlreadyHeld)
|
|
1232
1249
|
return applyTaskEventUnlocked(request, options);
|
|
1233
|
-
if (request.dryRun ||
|
|
1250
|
+
if (request.dryRun || !['started', 'completed'].includes(parts.phase)) {
|
|
1234
1251
|
return applyTaskEventUnlocked(request, options);
|
|
1235
1252
|
}
|
|
1236
1253
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const OPTIONAL_PLATFORM_AUDITS = new Set([
|
|
2
|
+
'closed-status-labels', 'status-label', 'comment-marker', 'pr-comment-marker',
|
|
3
|
+
'pr-comment-last-commit', 'pr-comment-content', 'comment-content', 'task-comment-content',
|
|
4
|
+
'in-labels-computed', 'pr-type-label', 'in-labels-match-pr', 'pr-assignee',
|
|
5
|
+
'requirements', 'issue-type', 'issue-fields', 'milestone'
|
|
6
|
+
]);
|
|
7
|
+
const STATUS_LABELS = {
|
|
8
|
+
'create-task': 'status: waiting-for-triage',
|
|
9
|
+
'analyze-task': 'status: pending-design-work',
|
|
10
|
+
'review-analysis': 'status: pending-design-work',
|
|
11
|
+
'plan-task': 'status: pending-design-work',
|
|
12
|
+
'review-plan': 'status: pending-design-work',
|
|
13
|
+
'code-task': 'status: in-progress',
|
|
14
|
+
'review-code': 'status: in-progress'
|
|
15
|
+
};
|
|
16
|
+
const ARTIFACT_COMMENT_SKILLS = new Set([
|
|
17
|
+
'analyze-task', 'plan-task', 'review-analysis', 'review-plan', 'code-task',
|
|
18
|
+
'review-code', 'review-pr', 'run-manual-validation'
|
|
19
|
+
]);
|
|
20
|
+
const PR_AUDIT_SKILLS = new Set(['create-pr']);
|
|
21
|
+
const IN_LABEL_AUDIT_SKILLS = new Set(['code-task', 'commit', 'create-pr']);
|
|
22
|
+
export function platformAuditPolicy(checkId, context) {
|
|
23
|
+
const skillName = context.skillName || '';
|
|
24
|
+
const artifactComment = Boolean(context.artifactFile) && ARTIFACT_COMMENT_SKILLS.has(skillName);
|
|
25
|
+
const prAudit = PR_AUDIT_SKILLS.has(skillName);
|
|
26
|
+
const inLabelAudit = IN_LABEL_AUDIT_SKILLS.has(skillName);
|
|
27
|
+
switch (checkId) {
|
|
28
|
+
case 'status-label':
|
|
29
|
+
return { classification: 'soft', enabled: Boolean(STATUS_LABELS[skillName]), expectedStatusLabel: STATUS_LABELS[skillName] };
|
|
30
|
+
case 'comment-marker':
|
|
31
|
+
return {
|
|
32
|
+
classification: 'soft',
|
|
33
|
+
enabled: artifactComment || skillName === 'complete-task',
|
|
34
|
+
...(artifactComment ? { expectedCommentMarkerKey: 'artifact' } : skillName === 'complete-task' ? { expectedCommentMarkerKey: 'summary' } : {})
|
|
35
|
+
};
|
|
36
|
+
case 'pr-comment-marker':
|
|
37
|
+
return {
|
|
38
|
+
classification: 'soft',
|
|
39
|
+
enabled: skillName === 'commit' || prAudit,
|
|
40
|
+
...(skillName === 'commit' || prAudit ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
41
|
+
};
|
|
42
|
+
case 'pr-comment-last-commit':
|
|
43
|
+
return {
|
|
44
|
+
classification: 'soft',
|
|
45
|
+
enabled: skillName === 'commit',
|
|
46
|
+
...(skillName === 'commit' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
47
|
+
};
|
|
48
|
+
case 'pr-comment-content':
|
|
49
|
+
return {
|
|
50
|
+
classification: 'soft',
|
|
51
|
+
enabled: skillName === 'complete-manual-validation',
|
|
52
|
+
...(skillName === 'complete-manual-validation' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
53
|
+
};
|
|
54
|
+
case 'comment-content':
|
|
55
|
+
return { classification: 'soft', enabled: artifactComment, expectedCommentMarkerKey: 'artifact' };
|
|
56
|
+
case 'task-comment-content':
|
|
57
|
+
return { classification: 'soft', enabled: true };
|
|
58
|
+
case 'in-labels-computed':
|
|
59
|
+
return { classification: 'soft', enabled: inLabelAudit };
|
|
60
|
+
case 'pr-type-label':
|
|
61
|
+
case 'in-labels-match-pr':
|
|
62
|
+
case 'pr-assignee':
|
|
63
|
+
return { classification: 'soft', enabled: prAudit };
|
|
64
|
+
case 'requirements':
|
|
65
|
+
return { classification: 'soft', enabled: skillName === 'complete-task' };
|
|
66
|
+
case 'milestone':
|
|
67
|
+
return { classification: 'soft', enabled: true, requireSpecificMilestone: skillName === 'code-task' };
|
|
68
|
+
case 'closed-status-labels':
|
|
69
|
+
case 'issue-type':
|
|
70
|
+
case 'issue-fields':
|
|
71
|
+
return { classification: 'soft', enabled: true };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function classificationForCheck(checkId) {
|
|
75
|
+
return checkId.startsWith('platform.') && OPTIONAL_PLATFORM_AUDITS.has(checkId.slice('platform.'.length))
|
|
76
|
+
? 'soft'
|
|
77
|
+
: 'hard';
|
|
78
|
+
}
|
|
79
|
+
export function effectiveStatus(status, classification) {
|
|
80
|
+
return classification === 'hard' ? status : 'pass';
|
|
81
|
+
}
|
|
82
|
+
export function normalizeVerificationRecord(record) {
|
|
83
|
+
const checkId = record.checkId ?? record.type;
|
|
84
|
+
const classification = record.classification ?? classificationForCheck(checkId);
|
|
85
|
+
return {
|
|
86
|
+
...record,
|
|
87
|
+
checkId,
|
|
88
|
+
classification,
|
|
89
|
+
effectiveStatus: record.effectiveStatus ?? effectiveStatus(record.status, classification),
|
|
90
|
+
reason: record.reason ?? record.fail_type ?? (classification === 'info' ? 'NOT_APPLICABLE' : record.status === 'pass' ? 'OK' : 'CHECK_FAILED'),
|
|
91
|
+
action: record.action ?? (classification === 'info' || record.status === 'pass' ? 'No action required' : 'Review the check evidence and retry after correction')
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=gate-policy.js.map
|
|
@@ -292,12 +292,37 @@ function cleanupConsumedRecoveryFiles(repoRoot, task, candidate) {
|
|
|
292
292
|
return false;
|
|
293
293
|
}
|
|
294
294
|
for (const entry of entries) {
|
|
295
|
-
if (entry.isSymbolicLink()
|
|
295
|
+
if (entry.isSymbolicLink())
|
|
296
|
+
return false;
|
|
297
|
+
if (entry.isFile()) {
|
|
298
|
+
if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name))
|
|
299
|
+
return false;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!entry.isDirectory() || entry.name !== 'generations')
|
|
303
|
+
return false;
|
|
304
|
+
let generations;
|
|
305
|
+
try {
|
|
306
|
+
generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (generations.some((generation) => generation.isSymbolicLink()
|
|
312
|
+
|| !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name)))
|
|
296
313
|
return false;
|
|
297
314
|
}
|
|
298
315
|
try {
|
|
299
|
-
for (const entry of entries)
|
|
300
|
-
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
const target = path.join(recovery, entry.name);
|
|
318
|
+
if (entry.isFile())
|
|
319
|
+
fs.unlinkSync(target);
|
|
320
|
+
else {
|
|
321
|
+
for (const generation of fs.readdirSync(target, { withFileTypes: true }))
|
|
322
|
+
fs.unlinkSync(path.join(target, generation.name));
|
|
323
|
+
fs.rmdirSync(target);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
301
326
|
fs.rmdirSync(recovery);
|
|
302
327
|
const recoveryRoot = path.dirname(recovery);
|
|
303
328
|
if (fs.readdirSync(recoveryRoot).length === 0)
|
|
@@ -2,13 +2,13 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { inspectDecisionDetailDuplicates } from './decision-details.js';
|
|
3
3
|
import { scanVisibleMarkdown } from './markdown.js';
|
|
4
4
|
import { parseArtifactName } from './artifact-name.js';
|
|
5
|
-
import { hasOpenArtifactRound,
|
|
5
|
+
import { hasOpenArtifactRound, validateArtifactPublication, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
6
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
7
7
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
8
8
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.js';
|
|
9
9
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
10
10
|
import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
11
|
-
import { beginArtifactRecovery, commitArtifactRecovery, consumeArtifactRecovery, prepareArtifactRecoveryCommit, recordArtifactRecoveryPassed, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
|
|
11
|
+
import { beginArtifactRecovery, commitArtifactRecovery, consumeArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, recordArtifactRecoveryPassed, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
|
|
12
12
|
import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
|
|
13
13
|
const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
|
|
14
14
|
analysis: getArtifactSchema('analysis').sections.map((section) => section.headings.zh),
|
|
@@ -97,13 +97,16 @@ function validateLocalArtifact(content, options) {
|
|
|
97
97
|
if (!decisionDetails.ok)
|
|
98
98
|
diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
99
99
|
if (options.taskContent !== undefined) {
|
|
100
|
-
const
|
|
101
|
-
|
|
100
|
+
const identity = options.artifact ? parseArtifactName(options.artifact) : null;
|
|
101
|
+
const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
|
|
102
|
+
? expectedQualificationRelations(options.taskContent, options.family)
|
|
103
|
+
: undefined;
|
|
104
|
+
if (expected && !expected.ok)
|
|
102
105
|
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
103
106
|
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
104
107
|
family: options.family,
|
|
105
108
|
artifact: options.artifact,
|
|
106
|
-
expectedUpstreamRelations: expected
|
|
109
|
+
expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
|
|
107
110
|
});
|
|
108
111
|
if (!qualification.ok)
|
|
109
112
|
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
@@ -136,7 +139,7 @@ function tupleFor(request, taskId, authority) {
|
|
|
136
139
|
authorityDigest: authority ? authorityDigest(authority) : null
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
|
-
function prepareLocalArtifact(request, candidate, authority) {
|
|
142
|
+
function prepareLocalArtifact(request, candidate, authority, preflightOnly = false) {
|
|
140
143
|
const failed = (code, message, extra = {}) => ({
|
|
141
144
|
result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
|
|
142
145
|
});
|
|
@@ -152,14 +155,16 @@ function prepareLocalArtifact(request, candidate, authority) {
|
|
|
152
155
|
let taskContent;
|
|
153
156
|
let content;
|
|
154
157
|
let recovery;
|
|
158
|
+
let recoveryState;
|
|
155
159
|
try {
|
|
156
160
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
157
161
|
if (request.recoveryId) {
|
|
158
162
|
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
159
163
|
if (!intent || intent.recoveryOperationId !== request.recoveryId)
|
|
160
164
|
return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'recovery id does not match the artifact journal');
|
|
161
|
-
if (!['awaiting-recovery', '
|
|
165
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
|
|
162
166
|
return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
|
|
167
|
+
recoveryState = intent.state;
|
|
163
168
|
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
164
169
|
content = fs.existsSync(recovery.stagingPath)
|
|
165
170
|
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
@@ -192,24 +197,63 @@ function prepareLocalArtifact(request, candidate, authority) {
|
|
|
192
197
|
if (request.recoveryId) {
|
|
193
198
|
if (!validation.ok)
|
|
194
199
|
return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
200
|
+
if (preflightOnly && recovery && recoveryState === 'awaiting-preflight-recovery') {
|
|
201
|
+
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
202
|
+
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
203
|
+
}
|
|
195
204
|
return {
|
|
196
|
-
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(recovery) },
|
|
205
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(recovery) },
|
|
197
206
|
content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
198
207
|
};
|
|
199
208
|
}
|
|
200
209
|
const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
201
210
|
if (existing?.state === 'passed' || existing?.state === 'consumed') {
|
|
202
|
-
if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
|
|
211
|
+
if (existing.state === 'consumed' && (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest)) {
|
|
203
212
|
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
204
213
|
}
|
|
214
|
+
if (existing.finalArtifactSha256 === artifactSha256 && existing.finalSemanticDigest === validation.semanticDigest) {
|
|
215
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
216
|
+
return {
|
|
217
|
+
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
|
|
218
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (existing && ['preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready'].includes(existing.state)) {
|
|
223
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
224
|
+
return {
|
|
225
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
|
|
226
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (existing?.state === 'commit-started') {
|
|
230
|
+
if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
|
|
231
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match the interrupted recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
232
|
+
}
|
|
205
233
|
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
234
|
+
const reconciled = reconcileArtifactRecovery(context, {
|
|
235
|
+
lockAlreadyHeld: request.lockAlreadyHeld,
|
|
236
|
+
validateFinal: () => validation.semanticDigest === existing.finalSemanticDigest
|
|
237
|
+
});
|
|
238
|
+
if (reconciled.status !== 'passed') {
|
|
239
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `interrupted recovery could not be reconciled: ${reconciled.status}`), content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
240
|
+
}
|
|
206
241
|
return {
|
|
207
|
-
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
|
|
242
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
|
|
208
243
|
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
209
244
|
};
|
|
210
245
|
}
|
|
211
246
|
try {
|
|
212
247
|
if (validation.ok) {
|
|
248
|
+
if (preflightOnly) {
|
|
249
|
+
const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
|
|
250
|
+
repoRoot: resolved.repoRoot, taskDir: resolved.taskDir,
|
|
251
|
+
...(authority ? { recoveryId: authority.operationId } : {}), lockAlreadyHeld: request.lockAlreadyHeld
|
|
252
|
+
});
|
|
253
|
+
const staged = stageArtifactCandidate(context, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
254
|
+
prepareArtifactRecoveryCommit(context, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
255
|
+
return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
256
|
+
}
|
|
213
257
|
const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
|
|
214
258
|
repoRoot: resolved.repoRoot,
|
|
215
259
|
taskDir: resolved.taskDir,
|
|
@@ -219,7 +263,7 @@ function prepareLocalArtifact(request, candidate, authority) {
|
|
|
219
263
|
lockAlreadyHeld: request.lockAlreadyHeld
|
|
220
264
|
});
|
|
221
265
|
recovery = context;
|
|
222
|
-
return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
266
|
+
return { result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
223
267
|
}
|
|
224
268
|
const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
|
|
225
269
|
repoRoot: resolved.repoRoot,
|
|
@@ -237,7 +281,7 @@ function prepareLocalArtifact(request, candidate, authority) {
|
|
|
237
281
|
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', String(error)), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
238
282
|
}
|
|
239
283
|
}
|
|
240
|
-
function commitLocalArtifactProvenance(prepared) {
|
|
284
|
+
function commitLocalArtifactProvenance(prepared, options = {}) {
|
|
241
285
|
if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery)
|
|
242
286
|
return prepared.result;
|
|
243
287
|
if (prepared.result.status === 'failed')
|
|
@@ -250,30 +294,48 @@ function commitLocalArtifactProvenance(prepared) {
|
|
|
250
294
|
if (intent.state === 'passed' || intent.state === 'consumed') {
|
|
251
295
|
return {
|
|
252
296
|
...prepared.result,
|
|
297
|
+
changed: false,
|
|
253
298
|
artifactSha256: intent.finalArtifactSha256,
|
|
254
299
|
semanticDigest: intent.finalSemanticDigest,
|
|
255
300
|
error: null
|
|
256
301
|
};
|
|
257
302
|
}
|
|
258
|
-
else if (intent.state === '
|
|
259
|
-
|
|
303
|
+
else if (intent.state === 'commit-started') {
|
|
304
|
+
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
305
|
+
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
306
|
+
afterPublish: options.afterPublish
|
|
307
|
+
});
|
|
308
|
+
committed = reconciled.intent;
|
|
260
309
|
}
|
|
261
|
-
else {
|
|
310
|
+
else if (intent.state === 'awaiting-preflight-recovery') {
|
|
262
311
|
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
263
312
|
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
264
313
|
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
265
314
|
}
|
|
266
|
-
if (
|
|
267
|
-
|
|
315
|
+
if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
316
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
317
|
+
}
|
|
318
|
+
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
319
|
+
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
320
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
321
|
+
}
|
|
322
|
+
else if (intent.state === 'full-finalizer-ready') {
|
|
323
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
324
|
+
}
|
|
325
|
+
if (!committed || committed.state !== 'passed')
|
|
326
|
+
throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
268
327
|
return {
|
|
269
328
|
...prepared.result,
|
|
270
329
|
status: 'passed',
|
|
330
|
+
changed: false,
|
|
271
331
|
artifactSha256: committed.finalArtifactSha256,
|
|
272
332
|
semanticDigest: committed.finalSemanticDigest,
|
|
273
333
|
error: null
|
|
274
334
|
};
|
|
275
335
|
}
|
|
276
336
|
catch (error) {
|
|
337
|
+
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
|
|
338
|
+
throw error;
|
|
277
339
|
return { ...prepared.result, status: 'failed', error: {
|
|
278
340
|
code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
|
|
279
341
|
} };
|
|
@@ -295,5 +357,10 @@ function finalizeLocalArtifact(request, authority, options = {}) {
|
|
|
295
357
|
}
|
|
296
358
|
return result;
|
|
297
359
|
}
|
|
298
|
-
|
|
360
|
+
/** Validate and seal an immutable preflight generation without publishing the formal artifact. */
|
|
361
|
+
function preflightLocalArtifact(request, authority) {
|
|
362
|
+
const prepared = prepareLocalArtifact(request, undefined, authority, true);
|
|
363
|
+
return prepared.result;
|
|
364
|
+
}
|
|
365
|
+
export { LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, preflightLocalArtifact, prepareLocalArtifact, commitLocalArtifactProvenance, readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent, canonicalSemanticDigest as semanticDigest, sha256Content, validateLocalArtifact };
|
|
299
366
|
//# sourceMappingURL=local-artifact-finalization.js.map
|