@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
|
@@ -20,7 +20,7 @@ import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead,
|
|
|
20
20
|
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.js';
|
|
21
21
|
import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.js';
|
|
22
22
|
import { hasOpenLifecycleExecution } from './activity-log.js';
|
|
23
|
-
import {
|
|
23
|
+
import { reconcileTaskInvalidation } from './invalidation-command.js';
|
|
24
24
|
import { normalizeAgentToken } from '../agent-clients/tokens.js';
|
|
25
25
|
function supportsLifecycleDelegation(client) {
|
|
26
26
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
@@ -511,14 +511,12 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
511
511
|
}
|
|
512
512
|
let metadata;
|
|
513
513
|
let currentLedger = null;
|
|
514
|
-
let currentInvalidation = { operations: [], targets: [] };
|
|
515
514
|
if (resolved.state === 'active') {
|
|
516
515
|
const contract = validateCurrentTaskContract(content);
|
|
517
516
|
if (!contract.ok)
|
|
518
517
|
return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
|
|
519
518
|
metadata = contract.metadata;
|
|
520
519
|
currentLedger = contract.ledger;
|
|
521
|
-
currentInvalidation = contract.invalidation;
|
|
522
520
|
}
|
|
523
521
|
else {
|
|
524
522
|
try {
|
|
@@ -528,9 +526,6 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
528
526
|
return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
529
527
|
}
|
|
530
528
|
}
|
|
531
|
-
if (invalidationBlocks(currentInvalidation)) {
|
|
532
|
-
return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
|
|
533
|
-
}
|
|
534
529
|
let run;
|
|
535
530
|
try {
|
|
536
531
|
run = readRun(resolved.taskDir, options);
|
|
@@ -545,6 +540,9 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
545
540
|
if (!routed)
|
|
546
541
|
return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
|
|
547
542
|
if ('completion' in routed) {
|
|
543
|
+
if (facts.facts.executionBusy && !run?.pendingDelegation) {
|
|
544
|
+
return failed('ORCHESTRATION_EXECUTION_BUSY', 'clean completion requires no open lifecycle execution', resolved.taskId);
|
|
545
|
+
}
|
|
548
546
|
const reviewRound = maxArtifactRound(fs.readdirSync(resolved.taskDir), 'review-code');
|
|
549
547
|
const review = parseReviewSummary(fs.readFileSync(path.join(resolved.taskDir, artifactName('review-code', reviewRound)), 'utf8'));
|
|
550
548
|
if (!review.ok || review.summary.manualValidation === null) {
|
|
@@ -670,6 +668,10 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
670
668
|
}
|
|
671
669
|
if (run.stepCount >= run.maxSteps)
|
|
672
670
|
return pauseOrchestration(taskRef, 'ORCHESTRATION_MAX_STEPS', 'maximum orchestration steps reached', true, options);
|
|
671
|
+
const invalidation = reconcileTaskInvalidation(taskRef, { repoRoot: resolved.repoRoot });
|
|
672
|
+
if (invalidation.status === 'failed') {
|
|
673
|
+
return failed('ORCHESTRATION_INVALIDATION_RECONCILE_FAILED', `${invalidation.error?.code ?? 'UNKNOWN'}: ${invalidation.error?.message ?? ''}`, resolved.taskId);
|
|
674
|
+
}
|
|
673
675
|
const routed = routeOrchestration(taskRef, options);
|
|
674
676
|
if (!routed.next)
|
|
675
677
|
return routed;
|
|
@@ -118,20 +118,26 @@ function parseReviewSummary(content) {
|
|
|
118
118
|
function equalCounts(left, right) {
|
|
119
119
|
return left.blocker === right.blocker && left.major === right.major && left.minor === right.minor;
|
|
120
120
|
}
|
|
121
|
-
function finalizeReviewSummaryContent(content, counts) {
|
|
121
|
+
function finalizeReviewSummaryContent(content, counts, options = {}) {
|
|
122
122
|
const parsed = parseReviewSummary(content);
|
|
123
123
|
if (!parsed.ok)
|
|
124
124
|
return parsed;
|
|
125
125
|
if (parsed.summary.countState === 'numeric') {
|
|
126
126
|
if (equalCounts(parsed.summary.counts, counts))
|
|
127
127
|
return { ok: true, changed: false, content };
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
if (!options.refreshNumericCounts)
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
code: 'REVIEW_SUMMARY_COUNT_MISMATCH',
|
|
132
|
+
message: 'review summary finding counts do not match the ledger snapshot'
|
|
133
|
+
};
|
|
133
134
|
}
|
|
134
135
|
let line = content.slice(parsed.findingsStart, parsed.findingsEnd);
|
|
136
|
+
if (parsed.summary.countState === 'numeric') {
|
|
137
|
+
const values = [counts.blocker, counts.major, counts.minor];
|
|
138
|
+
let ordinal = 0;
|
|
139
|
+
line = line.replace(/\d+(?=\s*(?:阻塞项|主要|次要|blockers?\b|majors?\b|minors?\b))/gi, () => String(values[ordinal++]));
|
|
140
|
+
}
|
|
135
141
|
line = line
|
|
136
142
|
.replace(PLACEHOLDERS[0], String(counts.blocker))
|
|
137
143
|
.replace(PLACEHOLDERS[1], String(counts.major))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function parseReviewCommand(args) {
|
|
2
|
-
if (!args[0] || args[1]
|
|
3
|
-
throw new Error('task ref and
|
|
2
|
+
if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? ''))
|
|
3
|
+
throw new Error('task ref and a supported review operation are required');
|
|
4
4
|
const values = {};
|
|
5
5
|
const flags = {
|
|
6
6
|
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
|
|
@@ -10,8 +10,9 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
10
10
|
import { allowsManualOverride } from './guard-override.js';
|
|
11
11
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
12
12
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
13
|
+
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
13
14
|
import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
14
|
-
import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryCommit, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
|
|
15
|
+
import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
|
|
15
16
|
const STAGES = {
|
|
16
17
|
analysis: { family: 'review-analysis' },
|
|
17
18
|
plan: { family: 'review-plan' },
|
|
@@ -37,6 +38,154 @@ function failed(request, code, message, taskId = null, stageStatus = null, extra
|
|
|
37
38
|
...extra
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
function preflightFailed(request, code, message, taskId = null, extra = {}) {
|
|
42
|
+
const { status: _status, intent: _intent, stageStatus: _stageStatus, ...finalizationExtra } = extra;
|
|
43
|
+
return {
|
|
44
|
+
...failed(request, code, message, taskId, null, finalizationExtra),
|
|
45
|
+
status: 'failed',
|
|
46
|
+
intent: 'preflight',
|
|
47
|
+
stageStatus: null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Validate and seal a review draft before any finding-ledger write. */
|
|
51
|
+
function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
52
|
+
const stage = request.stage;
|
|
53
|
+
const spec = STAGES[stage];
|
|
54
|
+
if (!spec)
|
|
55
|
+
return preflightFailed(request, 'REVIEW_STAGE_INVALID', `unsupported review stage '${request.stage}'`);
|
|
56
|
+
if (!request.taskRef || !request.artifact)
|
|
57
|
+
return preflightFailed(request, 'REVIEW_PAYLOAD_INVALID', 'taskRef, stage, and artifact are required');
|
|
58
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
59
|
+
if (!resolved.ok)
|
|
60
|
+
return preflightFailed(request, resolved.code, resolved.message, resolved.taskId ?? null);
|
|
61
|
+
if (resolved.state !== 'active')
|
|
62
|
+
return preflightFailed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
63
|
+
const parsed = parseArtifactName(request.artifact);
|
|
64
|
+
if (!parsed || parsed.family !== spec.family)
|
|
65
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${spec.family}`, resolved.taskId);
|
|
66
|
+
const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
|
|
67
|
+
if (publicationError)
|
|
68
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
|
|
69
|
+
let taskContent;
|
|
70
|
+
let content;
|
|
71
|
+
let recovery;
|
|
72
|
+
try {
|
|
73
|
+
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
74
|
+
if (request.recoveryId) {
|
|
75
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
76
|
+
if (!intent || intent.recoveryOperationId !== request.recoveryId)
|
|
77
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
|
|
78
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state))
|
|
79
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
|
|
80
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
81
|
+
content = fs.existsSync(recovery.stagingPath)
|
|
82
|
+
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
83
|
+
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
87
|
+
if (!validated.ok)
|
|
88
|
+
return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
89
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
90
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
91
|
+
if (intent?.state === 'preflight-ready')
|
|
92
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
93
|
+
if (intent?.state === 'consumed')
|
|
94
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId);
|
|
99
|
+
}
|
|
100
|
+
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round))
|
|
101
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
|
|
102
|
+
const execution = validateLifecycleExecution(request.taskRef, {
|
|
103
|
+
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
104
|
+
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
105
|
+
dryRun: request.dryRun
|
|
106
|
+
}, { repoRoot: options.repoRoot });
|
|
107
|
+
if (!execution.ok)
|
|
108
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${execution.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH'}: ${execution.error?.message ?? 'orchestration provenance validation failed'}`, resolved.taskId);
|
|
109
|
+
const artifactSha256 = sha256Content(content);
|
|
110
|
+
const semanticDigest = canonicalSemanticDigest(content);
|
|
111
|
+
const detail = inspectDecisionDetailDuplicates(content);
|
|
112
|
+
const structure = inspectArtifactContract(content, getArtifactSchema(spec.family));
|
|
113
|
+
const expected = expectedQualificationRelations(taskContent, spec.family);
|
|
114
|
+
if (!expected.ok)
|
|
115
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
|
|
116
|
+
const qualification = validateQualificationAudit(taskContent, content, {
|
|
117
|
+
family: spec.family,
|
|
118
|
+
artifact: request.artifact,
|
|
119
|
+
expectedUpstreamRelations: expected.relations
|
|
120
|
+
});
|
|
121
|
+
const qualificationError = qualification.ok ? null : qualification;
|
|
122
|
+
if (!detail.ok || !structure.ok || qualificationError) {
|
|
123
|
+
if (!recovery && !request.dryRun) {
|
|
124
|
+
try {
|
|
125
|
+
recovery = beginArtifactRecovery({
|
|
126
|
+
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
127
|
+
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
128
|
+
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const diagnostics = [
|
|
135
|
+
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
136
|
+
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
|
|
137
|
+
...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
|
|
138
|
+
].join('; ');
|
|
139
|
+
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
|
|
140
|
+
artifactSha256, semanticDigest,
|
|
141
|
+
...(recovery ? { recovery: recoveryInfo(recovery) } : {})
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
|
|
145
|
+
return {
|
|
146
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
|
|
147
|
+
status: 'passed', changed: false, error: null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
recovery ??= beginArtifactRecovery({
|
|
152
|
+
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
153
|
+
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
154
|
+
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
155
|
+
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
156
|
+
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
157
|
+
return {
|
|
158
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
|
|
159
|
+
status: 'passed', changed: false, error: null
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function preflightReviewSummary(request, options = {}) {
|
|
167
|
+
if (request.dryRun || options.lockAlreadyHeld)
|
|
168
|
+
return preflightReviewSummaryUnlocked(request, options);
|
|
169
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
170
|
+
if (!resolved.ok)
|
|
171
|
+
return preflightReviewSummaryUnlocked(request, options);
|
|
172
|
+
try {
|
|
173
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.preflight', () => preflightReviewSummaryUnlocked(request, { ...options, lockAlreadyHeld: true }));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (!(error instanceof TaskExecutionLockError))
|
|
177
|
+
throw error;
|
|
178
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function recoveryInfo(recovery) {
|
|
182
|
+
return {
|
|
183
|
+
recoveryId: recovery.recoveryId,
|
|
184
|
+
candidatePath: recovery.stagingPath,
|
|
185
|
+
baselineSha256: recovery.baselineSha256,
|
|
186
|
+
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
187
|
+
};
|
|
188
|
+
}
|
|
40
189
|
/** Prepare domain validation, summary bytes and provenance before any publication. */
|
|
41
190
|
function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
42
191
|
let taskId = null;
|
|
@@ -76,7 +225,7 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
76
225
|
const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
|
|
77
226
|
if (!intent || intent.recoveryOperationId !== request.recoveryId)
|
|
78
227
|
return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
|
|
79
|
-
if (!['awaiting-recovery', '
|
|
228
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
|
|
80
229
|
return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
|
|
81
230
|
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
82
231
|
artifactContent = fs.existsSync(recovery.stagingPath)
|
|
@@ -97,11 +246,15 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
97
246
|
catch (error) {
|
|
98
247
|
return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
|
|
99
248
|
}
|
|
100
|
-
if (repairIntent?.state === '
|
|
249
|
+
if (repairIntent?.state === 'consumed') {
|
|
101
250
|
if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
|
|
102
251
|
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
103
252
|
}
|
|
104
253
|
}
|
|
254
|
+
if (repairIntent?.state === 'passed'
|
|
255
|
+
&& (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
|
|
256
|
+
repairIntent = undefined;
|
|
257
|
+
}
|
|
105
258
|
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
|
|
106
259
|
return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
|
|
107
260
|
}
|
|
@@ -155,10 +308,18 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
155
308
|
...(recovery ? { recovery } : {})
|
|
156
309
|
};
|
|
157
310
|
}
|
|
158
|
-
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts
|
|
311
|
+
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
|
|
312
|
+
refreshNumericCounts: Boolean(request.recoveryId && recovery)
|
|
313
|
+
});
|
|
159
314
|
if (!transformed.ok)
|
|
160
315
|
return reject(transformed.code, transformed.message);
|
|
161
316
|
const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
|
|
317
|
+
if (!recovery && repairIntent?.state === 'commit-started') {
|
|
318
|
+
if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
|
|
319
|
+
return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
|
|
320
|
+
}
|
|
321
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
|
|
322
|
+
}
|
|
162
323
|
if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
|
|
163
324
|
if (transformed.changed)
|
|
164
325
|
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
|
|
@@ -197,7 +358,7 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
197
358
|
lockAlreadyHeld: options.lockAlreadyHeld
|
|
198
359
|
};
|
|
199
360
|
}
|
|
200
|
-
function commitReviewSummaryProvenance(prepared, repoRoot) {
|
|
361
|
+
function commitReviewSummaryProvenance(prepared, repoRoot, options = {}) {
|
|
201
362
|
if (prepared.result.status === 'failed' || !prepared.recovery)
|
|
202
363
|
return prepared.result;
|
|
203
364
|
try {
|
|
@@ -205,16 +366,40 @@ function commitReviewSummaryProvenance(prepared, repoRoot) {
|
|
|
205
366
|
if (!intent)
|
|
206
367
|
throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
|
|
207
368
|
let committed;
|
|
208
|
-
if (intent.state === '
|
|
209
|
-
|
|
369
|
+
if (intent.state === 'passed' || intent.state === 'consumed') {
|
|
370
|
+
return {
|
|
371
|
+
...prepared.result,
|
|
372
|
+
status: 'no-op',
|
|
373
|
+
changed: false,
|
|
374
|
+
artifactSha256: intent.finalArtifactSha256,
|
|
375
|
+
semanticDigest: intent.finalSemanticDigest,
|
|
376
|
+
error: null
|
|
377
|
+
};
|
|
210
378
|
}
|
|
211
|
-
else {
|
|
379
|
+
else if (intent.state === 'commit-started') {
|
|
380
|
+
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
381
|
+
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
382
|
+
afterPublish: options.afterPublish
|
|
383
|
+
});
|
|
384
|
+
committed = reconciled.intent;
|
|
385
|
+
}
|
|
386
|
+
else if (intent.state === 'awaiting-preflight-recovery') {
|
|
212
387
|
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
213
388
|
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
214
389
|
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
215
390
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
391
|
+
else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
392
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
393
|
+
}
|
|
394
|
+
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
395
|
+
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
396
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
397
|
+
}
|
|
398
|
+
else if (intent.state === 'full-finalizer-ready') {
|
|
399
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
400
|
+
}
|
|
401
|
+
if (!committed || committed.state !== 'passed')
|
|
402
|
+
throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
218
403
|
return {
|
|
219
404
|
...prepared.result,
|
|
220
405
|
artifactSha256: committed.finalArtifactSha256,
|
|
@@ -223,6 +408,8 @@ function commitReviewSummaryProvenance(prepared, repoRoot) {
|
|
|
223
408
|
};
|
|
224
409
|
}
|
|
225
410
|
catch (error) {
|
|
411
|
+
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
|
|
412
|
+
throw error;
|
|
226
413
|
return { ...prepared.result, status: 'failed', error: {
|
|
227
414
|
code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
|
|
228
415
|
} };
|
|
@@ -268,5 +455,5 @@ function finalizeReviewSummary(request, options = {}) {
|
|
|
268
455
|
return failed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
|
|
269
456
|
}
|
|
270
457
|
}
|
|
271
|
-
export { finalizeReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
|
|
458
|
+
export { finalizeReviewSummary, preflightReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
|
|
272
459
|
//# sourceMappingURL=review-finalization.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
4
|
const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
|
|
5
5
|
const DIRECTORY = fs.constants.O_DIRECTORY ?? 0;
|
|
6
6
|
export class SecureFileError extends Error {
|
|
@@ -15,11 +15,8 @@ function conflict(message) {
|
|
|
15
15
|
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', message);
|
|
16
16
|
}
|
|
17
17
|
function sameStat(left, right) {
|
|
18
|
-
return left.dev === right.dev
|
|
19
|
-
&& left.
|
|
20
|
-
&& left.size === right.size
|
|
21
|
-
&& left.mtimeNs === right.mtimeNs
|
|
22
|
-
&& left.ctimeNs === right.ctimeNs;
|
|
18
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
19
|
+
&& left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
23
20
|
}
|
|
24
21
|
function assertNoSymlinkAncestors(candidate) {
|
|
25
22
|
let current = path.dirname(candidate);
|
|
@@ -36,15 +33,12 @@ function assertNoSymlinkAncestors(candidate) {
|
|
|
36
33
|
export async function readStableFile(candidate, options) {
|
|
37
34
|
return readStableFileSync(candidate, options);
|
|
38
35
|
}
|
|
39
|
-
/** Shared bounded reader for synchronous domain commands and async workflow callers. */
|
|
40
36
|
export function readStableFileSync(candidate, options) {
|
|
41
|
-
if (!path.isAbsolute(candidate))
|
|
37
|
+
if (!path.isAbsolute(candidate))
|
|
42
38
|
conflict('candidate path must be absolute and terminal');
|
|
43
|
-
}
|
|
44
39
|
assertNoSymlinkAncestors(candidate);
|
|
45
40
|
let descriptor;
|
|
46
41
|
try {
|
|
47
|
-
// O_NONBLOCK lets fstat reject a FIFO even when it has no writer.
|
|
48
42
|
descriptor = fs.openSync(candidate, fs.constants.O_RDONLY | NO_FOLLOW | (fs.constants.O_NONBLOCK ?? 0));
|
|
49
43
|
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
50
44
|
if (!before.isFile())
|
|
@@ -63,18 +57,16 @@ export function readStableFileSync(candidate, options) {
|
|
|
63
57
|
if (!sameStat(before, after))
|
|
64
58
|
conflict('candidate identity or metadata changed during read');
|
|
65
59
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
66
|
-
if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256)
|
|
60
|
+
if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256)
|
|
67
61
|
conflict('candidate digest does not match the expected digest');
|
|
68
|
-
}
|
|
69
62
|
return { bytes, sha256, stat: after };
|
|
70
63
|
}
|
|
71
64
|
catch (error) {
|
|
72
65
|
if (error instanceof SecureFileError)
|
|
73
66
|
throw error;
|
|
74
67
|
const code = error.code;
|
|
75
|
-
if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL')
|
|
68
|
+
if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL')
|
|
76
69
|
conflict('candidate terminal is not a stable non-symlink file');
|
|
77
|
-
}
|
|
78
70
|
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', error instanceof Error ? error.message : String(error));
|
|
79
71
|
}
|
|
80
72
|
finally {
|
|
@@ -93,9 +85,8 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
|
|
|
93
85
|
catch {
|
|
94
86
|
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is missing');
|
|
95
87
|
}
|
|
96
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
88
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
97
89
|
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is not a real directory');
|
|
98
|
-
}
|
|
99
90
|
const next = path.dirname(current);
|
|
100
91
|
if (next === current)
|
|
101
92
|
break;
|
|
@@ -103,9 +94,8 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
|
|
|
103
94
|
}
|
|
104
95
|
try {
|
|
105
96
|
const existing = await fs.promises.lstat(target);
|
|
106
|
-
if (existing.isSymbolicLink() || !existing.isFile())
|
|
97
|
+
if (existing.isSymbolicLink() || !existing.isFile())
|
|
107
98
|
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative target is not a regular file');
|
|
108
|
-
}
|
|
109
99
|
}
|
|
110
100
|
catch (error) {
|
|
111
101
|
if (error.code !== 'ENOENT')
|
|
@@ -120,7 +110,6 @@ export async function writeAtomicFile(target, bytes, mode = 0o600, expectedSha25
|
|
|
120
110
|
await handle.chmod(mode);
|
|
121
111
|
await handle.close();
|
|
122
112
|
handle = undefined;
|
|
123
|
-
// Check after all asynchronous preparation; keep the final check/rename adjacent.
|
|
124
113
|
if (expectedSha256 !== undefined)
|
|
125
114
|
readStableFileSync(target, { maxBytes: 1024 * 1024, expectedSha256 });
|
|
126
115
|
fs.renameSync(temporary, target);
|
|
@@ -29,6 +29,7 @@ import { validateLocalArtifact } from "./local-artifact-finalization.js";
|
|
|
29
29
|
import { validateQualificationAudit } from "./qualification-audit.js";
|
|
30
30
|
import { getArtifactSchema } from "./artifact-schema.js";
|
|
31
31
|
import { inspectArtifactContract } from "./artifact-operations.js";
|
|
32
|
+
import { normalizeVerificationRecord } from './gate-policy.js';
|
|
32
33
|
import { manualValidationFinalSummaryProjectionMatches } from "./manual-validation-receipt.js";
|
|
33
34
|
import { readManualValidationCompletion } from "./manual-validation-completion.js";
|
|
34
35
|
import { sha256File } from "./artifact-receipts.js";
|
|
@@ -1198,19 +1199,19 @@ function interpolate(template, taskDir, artifactFile) {
|
|
|
1198
1199
|
.replace(/\{artifact-stem\}/g, artifactStem);
|
|
1199
1200
|
}
|
|
1200
1201
|
function summarizeGate(checks) {
|
|
1201
|
-
if (checks.some((check) => check.
|
|
1202
|
+
if (checks.some((check) => check.effectiveStatus === "blocked")) {
|
|
1202
1203
|
return "blocked";
|
|
1203
1204
|
}
|
|
1204
|
-
if (checks.some((check) => check.
|
|
1205
|
+
if (checks.some((check) => check.effectiveStatus === "fail")) {
|
|
1205
1206
|
return "fail";
|
|
1206
1207
|
}
|
|
1207
1208
|
return "pass";
|
|
1208
1209
|
}
|
|
1209
1210
|
function summarizeChecks(checks) {
|
|
1210
1211
|
const counts = {
|
|
1211
|
-
pass: checks.filter((check) => check.
|
|
1212
|
-
fail: checks.filter((check) => check.
|
|
1213
|
-
blocked: checks.filter((check) => check.
|
|
1212
|
+
pass: checks.filter((check) => check.effectiveStatus === "pass").length,
|
|
1213
|
+
fail: checks.filter((check) => check.effectiveStatus === "fail").length,
|
|
1214
|
+
blocked: checks.filter((check) => check.effectiveStatus === "blocked").length
|
|
1214
1215
|
};
|
|
1215
1216
|
if (counts.blocked > 0) {
|
|
1216
1217
|
return `${counts.pass} passed, ${counts.fail} failed, ${counts.blocked} blocked`;
|
|
@@ -1221,7 +1222,7 @@ function buildAction(gate, checks) {
|
|
|
1221
1222
|
if (gate === "pass") {
|
|
1222
1223
|
return "All declared checks passed";
|
|
1223
1224
|
}
|
|
1224
|
-
const firstFailure = checks.find((check) => check.
|
|
1225
|
+
const firstFailure = checks.find((check) => check.effectiveStatus !== "pass");
|
|
1225
1226
|
if (!firstFailure) {
|
|
1226
1227
|
return "Review validation output";
|
|
1227
1228
|
}
|
|
@@ -1277,16 +1278,16 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
|
|
|
1277
1278
|
for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
|
|
1278
1279
|
if (checkConfig === null)
|
|
1279
1280
|
continue;
|
|
1280
|
-
const result = await runCheck(type, {
|
|
1281
|
+
const result = normalizeVerificationRecord(await runCheck(type, {
|
|
1281
1282
|
mode,
|
|
1282
1283
|
skillName,
|
|
1283
1284
|
taskDir: path.resolve(taskDir),
|
|
1284
1285
|
artifactFile,
|
|
1285
1286
|
repositoryRoot: repoRoot,
|
|
1286
1287
|
config: checkConfig
|
|
1287
|
-
}, shared);
|
|
1288
|
-
checks.push(result);
|
|
1289
|
-
if (
|
|
1288
|
+
}, shared));
|
|
1289
|
+
checks.push(...(result.subchecks?.map(normalizeVerificationRecord) ?? [result]));
|
|
1290
|
+
if (checks.at(-1)?.effectiveStatus === "blocked")
|
|
1290
1291
|
break;
|
|
1291
1292
|
}
|
|
1292
1293
|
const gate = summarizeGate(checks);
|
|
@@ -1298,15 +1299,25 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
|
|
|
1298
1299
|
return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
|
|
1299
1300
|
if (config === null)
|
|
1300
1301
|
return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
|
|
1302
|
+
const result = normalizeVerificationRecord(await runCheck(type, {
|
|
1303
|
+
skillName,
|
|
1304
|
+
taskDir: path.resolve(taskDir),
|
|
1305
|
+
artifactFile,
|
|
1306
|
+
repositoryRoot: repoRoot,
|
|
1307
|
+
config
|
|
1308
|
+
}, shared));
|
|
1309
|
+
if (!result.subchecks?.length)
|
|
1310
|
+
return { skill: skillName, ...result };
|
|
1311
|
+
const checks = result.subchecks.map(normalizeVerificationRecord);
|
|
1312
|
+
const gate = summarizeGate(checks);
|
|
1301
1313
|
return {
|
|
1302
1314
|
skill: skillName,
|
|
1303
|
-
...
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}, shared)
|
|
1315
|
+
...result,
|
|
1316
|
+
status: gate,
|
|
1317
|
+
effectiveStatus: gate,
|
|
1318
|
+
reason: gate === 'pass' ? 'OK' : checks.find((check) => check.effectiveStatus !== 'pass')?.reason ?? 'CHECK_FAILED',
|
|
1319
|
+
action: buildAction(gate, checks),
|
|
1320
|
+
checks
|
|
1310
1321
|
};
|
|
1311
1322
|
}
|
|
1312
1323
|
export { verifyInProcess };
|
|
@@ -80,7 +80,7 @@ async function verifyTaskEvent(request, options = {}) {
|
|
|
80
80
|
else {
|
|
81
81
|
for (const check of spec.checks ?? []) {
|
|
82
82
|
const payload = await engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
|
|
83
|
-
const status = payload.status;
|
|
83
|
+
const status = (payload.effectiveStatus ?? payload.status);
|
|
84
84
|
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
85
85
|
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
|
|
86
86
|
break;
|
|
@@ -103,19 +103,35 @@ function statusLabel(status) {
|
|
|
103
103
|
function renderPayload(payload) {
|
|
104
104
|
const lines = [];
|
|
105
105
|
if (Array.isArray(payload.checks)) {
|
|
106
|
-
|
|
106
|
+
const gateMode = typeof payload.gate === 'string';
|
|
107
|
+
lines.push(gateMode
|
|
108
|
+
? `Verification: ${payload.gate} | Skill: ${payload.skill}`
|
|
109
|
+
: `Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
|
|
107
110
|
for (const check of payload.checks) {
|
|
108
|
-
|
|
111
|
+
const raw = statusLabel(check.status);
|
|
112
|
+
const effective = statusLabel(check.effectiveStatus ?? check.status);
|
|
113
|
+
const reason = check.reason ?? 'CHECK_FAILED';
|
|
114
|
+
const message = check.message ?? reason;
|
|
115
|
+
lines.push(` [${effective}] ${check.checkId ?? check.type} (${check.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${message}; ${check.action ?? 'Review validation output'}`);
|
|
109
116
|
}
|
|
110
|
-
|
|
117
|
+
const summary = payload.summary ?? (() => {
|
|
118
|
+
const checks = payload.checks;
|
|
119
|
+
const statuses = checks.map((check) => check.effectiveStatus ?? check.status);
|
|
120
|
+
return `${statuses.filter((status) => status === 'pass').length} passed, ${statuses.filter((status) => status === 'fail').length} failed${statuses.includes('blocked') ? `, ${statuses.filter((status) => status === 'blocked').length} blocked` : ''}`;
|
|
121
|
+
})();
|
|
122
|
+
lines.push('', `Result: ${summary} - ${payload.action}`);
|
|
111
123
|
}
|
|
112
124
|
else {
|
|
113
125
|
lines.push(`Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const raw = statusLabel(payload.status);
|
|
127
|
+
const effective = statusLabel(payload.effectiveStatus ?? payload.status);
|
|
128
|
+
const reason = payload.reason ?? 'CHECK_FAILED';
|
|
129
|
+
lines.push(` [${effective}] ${payload.checkId ?? payload.type} (${payload.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${payload.message ?? reason}; ${payload.action ?? 'Review validation output'}`);
|
|
130
|
+
const status = payload.effectiveStatus ?? payload.status;
|
|
131
|
+
const summary = status === 'pass' ? '1 passed, 0 failed'
|
|
132
|
+
: status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
|
|
133
|
+
const action = status === 'pass' ? 'Requested check passed'
|
|
134
|
+
: status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
|
|
119
135
|
: `Fix ${payload.type} issues and re-run check`;
|
|
120
136
|
lines.push('', `Result: ${summary} - ${action}`);
|
|
121
137
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export const TASK_WORKFLOW_COMMANDS = Object.freeze({
|
|
2
2
|
'artifact-inspect': ['task-artifact', 'inspect'],
|
|
3
3
|
'artifact-init': ['task-artifact', 'init'],
|
|
4
|
+
'artifact-preflight': ['task-artifact', 'preflight'],
|
|
4
5
|
'artifact-finalize-local': ['task-artifact', 'finalize-local'],
|
|
6
|
+
'review-preflight': ['task-review', 'preflight'],
|
|
5
7
|
'review-finalize-summary': ['task-review', 'finalize-summary'],
|
|
6
8
|
event: ['task-event', null],
|
|
7
9
|
'ledger-finding-response': ['task-ledger', 'finding-respond'],
|