@fitlab-ai/agent-infra 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +4 -131
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -0
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +120 -13
- package/dist/lib/platform/issues.js +3 -4
- package/dist/lib/platform/provider-contract.d.ts +2 -0
- package/dist/lib/platform/provider-validation.js +7 -2
- package/dist/lib/platform/verification-sync.js +161 -156
- package/dist/lib/sandbox/control/executor.js +0 -38
- package/dist/lib/sandbox/control/finalization-response.js +10 -2
- package/dist/lib/sandbox/control/task-view.js +3 -1
- package/dist/lib/sandbox/control/workflow-executor.js +32 -41
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +27 -1
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +1 -8
- package/dist/lib/task/delegation-receipts.js +14 -76
- package/dist/lib/task/events.js +147 -278
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +273 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/local-artifact-finalization.js +31 -245
- package/dist/lib/task/orchestration.js +8 -24
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +47 -218
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lib/task/workflow-command.js +4 -1
- package/dist/lib/task/workflow-dispatch.js +55 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +3 -145
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -0
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +132 -13
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +178 -179
- package/lib/sandbox/control/executor.ts +0 -44
- package/lib/sandbox/control/finalization-response.ts +10 -2
- package/lib/sandbox/control/task-view.ts +3 -1
- package/lib/sandbox/control/workflow-executor.ts +31 -48
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +27 -1
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/delegation-receipts.ts +12 -79
- package/lib/task/events.ts +155 -287
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +277 -57
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/local-artifact-finalization.ts +29 -285
- package/lib/task/orchestration.ts +12 -41
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +46 -200
- 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 +4 -1
- package/lib/task/workflow-dispatch.ts +46 -3
- package/package.json +2 -2
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- 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 +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- 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/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- 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 +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- 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.zh-CN.md +4 -4
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -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 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- 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 +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -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/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
2
4
|
|
|
3
5
|
import { parseArtifactName } from './artifact-name.ts';
|
|
4
|
-
import { validateCompletedArtifact,
|
|
6
|
+
import { validateCompletedArtifact, validateArtifactPublication } from './artifact-lifecycle.ts';
|
|
5
7
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
6
8
|
import type { LedgerStageStatus, ReviewStage } from './ledger.ts';
|
|
7
9
|
import { finalizeReviewSummaryContent } from './review-artifacts.ts';
|
|
@@ -15,17 +17,6 @@ import type { ManualOverrideCapability } from './guard-override.ts';
|
|
|
15
17
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
16
18
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
17
19
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
18
|
-
import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
19
|
-
import {
|
|
20
|
-
beginArtifactRecovery,
|
|
21
|
-
commitArtifactRecovery,
|
|
22
|
-
prepareArtifactRecoveryFinal,
|
|
23
|
-
prepareArtifactRecoveryCommit,
|
|
24
|
-
reconcileArtifactRecovery,
|
|
25
|
-
recoveryContextFromIntent,
|
|
26
|
-
stageArtifactCandidate
|
|
27
|
-
} from './artifact-recovery.ts';
|
|
28
|
-
import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
|
|
29
20
|
|
|
30
21
|
type ReviewFinalizationErrorCode =
|
|
31
22
|
| ResolveTaskRefErrorCode
|
|
@@ -51,7 +42,6 @@ type ReviewFinalizationRequest = {
|
|
|
51
42
|
artifact: string;
|
|
52
43
|
orchestrated?: boolean;
|
|
53
44
|
dryRun?: boolean;
|
|
54
|
-
recoveryId?: string;
|
|
55
45
|
};
|
|
56
46
|
type ReviewFinalizationResult = {
|
|
57
47
|
status: 'planned' | 'applied' | 'no-op' | 'failed';
|
|
@@ -64,12 +54,6 @@ type ReviewFinalizationResult = {
|
|
|
64
54
|
stageStatus: LedgerStageStatus | null;
|
|
65
55
|
artifactSha256: string | null;
|
|
66
56
|
semanticDigest: string | null;
|
|
67
|
-
recovery?: Readonly<{
|
|
68
|
-
recoveryId: string;
|
|
69
|
-
candidatePath: string;
|
|
70
|
-
baselineSha256: string;
|
|
71
|
-
baselineSemanticDigest: string;
|
|
72
|
-
}>;
|
|
73
57
|
operations: readonly {
|
|
74
58
|
kind: 'artifact';
|
|
75
59
|
artifact: string;
|
|
@@ -90,7 +74,6 @@ type ReviewFinalizationOptions = {
|
|
|
90
74
|
fileSystem?: Partial<ReviewFileSystem>;
|
|
91
75
|
manualOverride?: ManualOverrideCapability;
|
|
92
76
|
lockAlreadyHeld?: boolean;
|
|
93
|
-
startRecovery?: boolean;
|
|
94
77
|
};
|
|
95
78
|
|
|
96
79
|
const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code' }> = {
|
|
@@ -143,7 +126,7 @@ function preflightFailed(
|
|
|
143
126
|
};
|
|
144
127
|
}
|
|
145
128
|
|
|
146
|
-
/** Validate
|
|
129
|
+
/** Validate the current review draft before any finding-ledger write. */
|
|
147
130
|
function preflightReviewSummaryUnlocked(
|
|
148
131
|
request: ReviewFinalizationRequest,
|
|
149
132
|
options: ReviewFinalizationOptions = {}
|
|
@@ -162,30 +145,13 @@ function preflightReviewSummaryUnlocked(
|
|
|
162
145
|
|
|
163
146
|
let taskContent: string;
|
|
164
147
|
let content: string;
|
|
165
|
-
let recovery: ArtifactRecoveryContext | undefined;
|
|
166
|
-
let existingIntent;
|
|
167
148
|
try {
|
|
168
149
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state)) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
|
|
173
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
174
|
-
content = fs.existsSync(recovery.stagingPath)
|
|
175
|
-
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
176
|
-
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
177
|
-
} else {
|
|
178
|
-
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
179
|
-
if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
180
|
-
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
181
|
-
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
182
|
-
existingIntent = intent;
|
|
183
|
-
if (intent?.state === 'preflight-ready') recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
184
|
-
if (intent?.state === 'consumed') return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
|
|
185
|
-
}
|
|
150
|
+
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
151
|
+
if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
152
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
186
153
|
} catch (error) { return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
|
|
187
154
|
|
|
188
|
-
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
|
|
189
155
|
const execution = validateLifecycleExecution(request.taskRef, {
|
|
190
156
|
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
191
157
|
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
@@ -206,14 +172,6 @@ function preflightReviewSummaryUnlocked(
|
|
|
206
172
|
});
|
|
207
173
|
const qualificationError = qualification.ok ? null : qualification;
|
|
208
174
|
if (!detail.ok || !structure.ok || qualificationError) {
|
|
209
|
-
if (!recovery && !request.dryRun) {
|
|
210
|
-
try {
|
|
211
|
-
recovery = beginArtifactRecovery({
|
|
212
|
-
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
213
|
-
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
214
|
-
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
215
|
-
} catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId); }
|
|
216
|
-
}
|
|
217
175
|
const diagnostics = [
|
|
218
176
|
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
219
177
|
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
|
|
@@ -221,33 +179,18 @@ function preflightReviewSummaryUnlocked(
|
|
|
221
179
|
].join('; ');
|
|
222
180
|
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
|
|
223
181
|
artifactSha256, semanticDigest,
|
|
224
|
-
...(recovery ? { recovery: recoveryInfo(recovery) } : {})
|
|
225
182
|
});
|
|
226
183
|
}
|
|
227
|
-
if (
|
|
184
|
+
if (request.dryRun) {
|
|
228
185
|
return {
|
|
229
186
|
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
|
|
230
187
|
status: 'passed', changed: false, error: null
|
|
231
188
|
};
|
|
232
189
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
recovery ??= beginArtifactRecovery({
|
|
241
|
-
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
242
|
-
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
243
|
-
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
244
|
-
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
245
|
-
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
246
|
-
return {
|
|
247
|
-
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
|
|
248
|
-
status: 'passed', changed: false, error: null
|
|
249
|
-
};
|
|
250
|
-
} catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest }); }
|
|
190
|
+
return {
|
|
191
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
|
|
192
|
+
status: 'passed', changed: false, error: null
|
|
193
|
+
};
|
|
251
194
|
}
|
|
252
195
|
|
|
253
196
|
function preflightReviewSummary(
|
|
@@ -273,19 +216,9 @@ function preflightReviewSummary(
|
|
|
273
216
|
type ReviewSummaryCandidatePreparation = Readonly<{
|
|
274
217
|
result: ReviewFinalizationResult;
|
|
275
218
|
content: string;
|
|
276
|
-
recovery?: ArtifactRecoveryContext;
|
|
277
219
|
lockAlreadyHeld?: boolean;
|
|
278
220
|
}>;
|
|
279
221
|
|
|
280
|
-
function recoveryInfo(recovery: ArtifactRecoveryContext): NonNullable<ReviewFinalizationResult['recovery']> {
|
|
281
|
-
return {
|
|
282
|
-
recoveryId: recovery.recoveryId,
|
|
283
|
-
candidatePath: recovery.stagingPath,
|
|
284
|
-
baselineSha256: recovery.baselineSha256,
|
|
285
|
-
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
222
|
/** Prepare domain validation, summary bytes and provenance before any publication. */
|
|
290
223
|
function prepareReviewSummaryCandidate(
|
|
291
224
|
request: ReviewFinalizationRequest,
|
|
@@ -314,36 +247,9 @@ function prepareReviewSummaryCandidate(
|
|
|
314
247
|
let taskContent: string;
|
|
315
248
|
try { taskContent = (options.fileSystem?.readFileSync ?? DEFAULT_FILE_SYSTEM.readFileSync)(resolved.taskMdPath); }
|
|
316
249
|
catch (error) { return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error)); }
|
|
317
|
-
let recovery: ArtifactRecoveryContext | undefined;
|
|
318
|
-
if (request.recoveryId) {
|
|
319
|
-
try {
|
|
320
|
-
const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact);
|
|
321
|
-
if (!intent || intent.recoveryOperationId !== request.recoveryId) return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
|
|
322
|
-
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
|
|
323
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
324
|
-
artifactContent = fs.existsSync(recovery.stagingPath)
|
|
325
|
-
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
326
|
-
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
327
|
-
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error)); }
|
|
328
|
-
}
|
|
329
250
|
const artifactSha256 = sha256Content(artifactContent);
|
|
330
251
|
const semanticDigest = canonicalSemanticDigest(artifactContent);
|
|
331
252
|
const digests = { artifactSha256, semanticDigest };
|
|
332
|
-
let repairIntent;
|
|
333
|
-
try { repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact); }
|
|
334
|
-
catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
|
|
335
|
-
if (repairIntent?.state === 'consumed') {
|
|
336
|
-
if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
|
|
337
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
if (repairIntent?.state === 'passed'
|
|
341
|
-
&& (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
|
|
342
|
-
repairIntent = undefined;
|
|
343
|
-
}
|
|
344
|
-
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
|
|
345
|
-
return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
|
|
346
|
-
}
|
|
347
253
|
const execution = validateLifecycleExecution(request.taskRef, {
|
|
348
254
|
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
349
255
|
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
@@ -364,57 +270,15 @@ function prepareReviewSummaryCandidate(
|
|
|
364
270
|
const schema = getArtifactSchema(spec.family)!;
|
|
365
271
|
const structure = inspectArtifactContract(artifactContent, schema);
|
|
366
272
|
if (!structure.ok) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
371
|
-
requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
|
|
372
|
-
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
373
|
-
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
|
|
374
|
-
}
|
|
375
|
-
return {
|
|
376
|
-
...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
377
|
-
artifactSha256, semanticDigest: structure.semanticDigest,
|
|
378
|
-
...(recovery ? { recovery: {
|
|
379
|
-
recoveryId: recovery.recoveryId,
|
|
380
|
-
candidatePath: recovery.stagingPath,
|
|
381
|
-
baselineSha256: recovery.baselineSha256,
|
|
382
|
-
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
383
|
-
} } : {})
|
|
384
|
-
}),
|
|
385
|
-
...(recovery ? { recovery } : {})
|
|
386
|
-
};
|
|
273
|
+
return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
274
|
+
artifactSha256, semanticDigest: structure.semanticDigest
|
|
275
|
+
});
|
|
387
276
|
}
|
|
388
|
-
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts
|
|
277
|
+
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
|
|
278
|
+
refreshNumericCounts: true
|
|
279
|
+
});
|
|
389
280
|
if (!transformed.ok) return reject(transformed.code, transformed.message);
|
|
390
281
|
const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
|
|
391
|
-
if (!recovery && repairIntent?.state === 'commit-started') {
|
|
392
|
-
if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
|
|
393
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
|
|
394
|
-
}
|
|
395
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
|
|
396
|
-
}
|
|
397
|
-
if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
|
|
398
|
-
if (transformed.changed) return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
|
|
399
|
-
return {
|
|
400
|
-
content: transformed.content,
|
|
401
|
-
result: {
|
|
402
|
-
...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
|
|
403
|
-
status: 'no-op',
|
|
404
|
-
changed: false,
|
|
405
|
-
error: null
|
|
406
|
-
},
|
|
407
|
-
lockAlreadyHeld: options.lockAlreadyHeld
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
if (!recovery && !request.dryRun && options.startRecovery) {
|
|
411
|
-
try {
|
|
412
|
-
recovery = beginArtifactRecovery({
|
|
413
|
-
taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
414
|
-
requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
|
|
415
|
-
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
416
|
-
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests); }
|
|
417
|
-
}
|
|
418
282
|
return {
|
|
419
283
|
content: transformed.content,
|
|
420
284
|
result: {
|
|
@@ -424,7 +288,6 @@ function prepareReviewSummaryCandidate(
|
|
|
424
288
|
operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
|
|
425
289
|
error: null
|
|
426
290
|
},
|
|
427
|
-
...(recovery ? { recovery } : {}),
|
|
428
291
|
lockAlreadyHeld: options.lockAlreadyHeld
|
|
429
292
|
};
|
|
430
293
|
}
|
|
@@ -434,51 +297,18 @@ function commitReviewSummaryProvenance(
|
|
|
434
297
|
repoRoot: string,
|
|
435
298
|
options: Readonly<{ afterPublish?: () => void }> = {}
|
|
436
299
|
): ReviewFinalizationResult {
|
|
437
|
-
|
|
300
|
+
return prepared.result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function replaceCurrentArtifactAtomically(file: string, content: string): void {
|
|
304
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
438
305
|
try {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
...prepared.result,
|
|
445
|
-
status: 'no-op',
|
|
446
|
-
changed: false,
|
|
447
|
-
artifactSha256: intent.finalArtifactSha256,
|
|
448
|
-
semanticDigest: intent.finalSemanticDigest,
|
|
449
|
-
error: null
|
|
450
|
-
};
|
|
451
|
-
} else if (intent.state === 'commit-started') {
|
|
452
|
-
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
453
|
-
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
454
|
-
afterPublish: options.afterPublish
|
|
455
|
-
});
|
|
456
|
-
committed = reconciled.intent;
|
|
457
|
-
} else if (intent.state === 'awaiting-preflight-recovery') {
|
|
458
|
-
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
459
|
-
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
460
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
461
|
-
} else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
462
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
463
|
-
}
|
|
464
|
-
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
465
|
-
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
466
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
467
|
-
} else if (intent.state === 'full-finalizer-ready') {
|
|
468
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
306
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
307
|
+
fs.renameSync(temporary, file);
|
|
308
|
+
} finally {
|
|
309
|
+
try { fs.unlinkSync(temporary); } catch (error) {
|
|
310
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
469
311
|
}
|
|
470
|
-
if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
471
|
-
return {
|
|
472
|
-
...prepared.result,
|
|
473
|
-
artifactSha256: committed.finalArtifactSha256,
|
|
474
|
-
semanticDigest: committed.finalSemanticDigest,
|
|
475
|
-
error: null
|
|
476
|
-
};
|
|
477
|
-
} catch (error) {
|
|
478
|
-
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
|
|
479
|
-
return { ...prepared.result, status: 'failed', error: {
|
|
480
|
-
code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
|
|
481
|
-
} };
|
|
482
312
|
}
|
|
483
313
|
}
|
|
484
314
|
|
|
@@ -500,14 +330,30 @@ function finalizeReviewSummaryUnlocked(
|
|
|
500
330
|
catch (error) { return failed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
|
|
501
331
|
const prepared = prepareReviewSummaryCandidate(request, content, options);
|
|
502
332
|
if (request.dryRun) return prepared.result;
|
|
503
|
-
|
|
333
|
+
if (prepared.result.status === 'failed' || !prepared.result.changed) return prepared.result;
|
|
334
|
+
try {
|
|
335
|
+
replaceCurrentArtifactAtomically(validated.artifact.path, prepared.content);
|
|
336
|
+
return {
|
|
337
|
+
...prepared.result,
|
|
338
|
+
status: 'applied',
|
|
339
|
+
artifactSha256: sha256Content(prepared.content),
|
|
340
|
+
semanticDigest: canonicalSemanticDigest(prepared.content)
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return failed(
|
|
344
|
+
request,
|
|
345
|
+
'REVIEW_RECOVERY_COMMIT_FAILED',
|
|
346
|
+
`cannot update current review artifact: ${error instanceof Error ? error.message : String(error)}`,
|
|
347
|
+
resolved.taskId
|
|
348
|
+
);
|
|
349
|
+
}
|
|
504
350
|
}
|
|
505
351
|
|
|
506
352
|
function finalizeReviewSummary(
|
|
507
353
|
request: ReviewFinalizationRequest,
|
|
508
354
|
options: ReviewFinalizationOptions = {}
|
|
509
355
|
): ReviewFinalizationResult {
|
|
510
|
-
const effectiveOptions =
|
|
356
|
+
const effectiveOptions = options;
|
|
511
357
|
if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
512
358
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
513
359
|
if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
@@ -44,6 +44,7 @@ import { validateQualificationAudit } from "./qualification-audit.ts";
|
|
|
44
44
|
import { getArtifactSchema } from "./artifact-schema.ts";
|
|
45
45
|
import { inspectArtifactContract } from "./artifact-operations.ts";
|
|
46
46
|
import type { VerificationShared } from "./verification-types.ts";
|
|
47
|
+
import { normalizeVerificationRecord } from './gate-policy.ts';
|
|
47
48
|
import { manualValidationFinalSummaryProjectionMatches } from "./manual-validation-receipt.ts";
|
|
48
49
|
import { readManualValidationCompletion } from "./manual-validation-completion.ts";
|
|
49
50
|
import { sha256File } from "./artifact-receipts.ts";
|
|
@@ -1392,11 +1393,11 @@ function interpolate(template: any, taskDir: any, artifactFile: any): any {
|
|
|
1392
1393
|
}
|
|
1393
1394
|
|
|
1394
1395
|
function summarizeGate(checks: any): any {
|
|
1395
|
-
if (checks.some((check: any) => check.
|
|
1396
|
+
if (checks.some((check: any) => check.effectiveStatus === "blocked")) {
|
|
1396
1397
|
return "blocked";
|
|
1397
1398
|
}
|
|
1398
1399
|
|
|
1399
|
-
if (checks.some((check: any) => check.
|
|
1400
|
+
if (checks.some((check: any) => check.effectiveStatus === "fail")) {
|
|
1400
1401
|
return "fail";
|
|
1401
1402
|
}
|
|
1402
1403
|
|
|
@@ -1405,9 +1406,9 @@ function summarizeGate(checks: any): any {
|
|
|
1405
1406
|
|
|
1406
1407
|
function summarizeChecks(checks: any): any {
|
|
1407
1408
|
const counts = {
|
|
1408
|
-
pass: checks.filter((check: any) => check.
|
|
1409
|
-
fail: checks.filter((check: any) => check.
|
|
1410
|
-
blocked: checks.filter((check: any) => check.
|
|
1409
|
+
pass: checks.filter((check: any) => check.effectiveStatus === "pass").length,
|
|
1410
|
+
fail: checks.filter((check: any) => check.effectiveStatus === "fail").length,
|
|
1411
|
+
blocked: checks.filter((check: any) => check.effectiveStatus === "blocked").length
|
|
1411
1412
|
};
|
|
1412
1413
|
|
|
1413
1414
|
if (counts.blocked > 0) {
|
|
@@ -1422,7 +1423,7 @@ function buildAction(gate: any, checks: any): any {
|
|
|
1422
1423
|
return "All declared checks passed";
|
|
1423
1424
|
}
|
|
1424
1425
|
|
|
1425
|
-
const firstFailure = checks.find((check: any) => check.
|
|
1426
|
+
const firstFailure = checks.find((check: any) => check.effectiveStatus !== "pass");
|
|
1426
1427
|
if (!firstFailure) {
|
|
1427
1428
|
return "Review validation output";
|
|
1428
1429
|
}
|
|
@@ -1484,16 +1485,16 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
|
|
|
1484
1485
|
const checks = [];
|
|
1485
1486
|
for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
|
|
1486
1487
|
if (checkConfig === null) continue;
|
|
1487
|
-
const result = await runCheck(type, {
|
|
1488
|
+
const result = normalizeVerificationRecord(await runCheck(type, {
|
|
1488
1489
|
mode,
|
|
1489
1490
|
skillName,
|
|
1490
1491
|
taskDir: path.resolve(taskDir),
|
|
1491
1492
|
artifactFile,
|
|
1492
1493
|
repositoryRoot: repoRoot,
|
|
1493
1494
|
config: checkConfig
|
|
1494
|
-
}, shared);
|
|
1495
|
-
checks.push(result);
|
|
1496
|
-
if (
|
|
1495
|
+
}, shared));
|
|
1496
|
+
checks.push(...(result.subchecks?.map(normalizeVerificationRecord) ?? [result]));
|
|
1497
|
+
if (checks.at(-1)?.effectiveStatus === "blocked") break;
|
|
1497
1498
|
}
|
|
1498
1499
|
const gate = summarizeGate(checks);
|
|
1499
1500
|
return { gate, skill: skillName, checks, summary: summarizeChecks(checks), action: buildAction(gate, checks) };
|
|
@@ -1502,15 +1503,24 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
|
|
|
1502
1503
|
const config = (verifyConfig.checks || {})[type];
|
|
1503
1504
|
if (config === undefined) return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
|
|
1504
1505
|
if (config === null) return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
|
|
1505
|
-
|
|
1506
|
-
skill: skillName,
|
|
1507
|
-
...await runCheck(type, {
|
|
1506
|
+
const result = normalizeVerificationRecord(await runCheck(type, {
|
|
1508
1507
|
skillName,
|
|
1509
1508
|
taskDir: path.resolve(taskDir),
|
|
1510
1509
|
artifactFile,
|
|
1511
1510
|
repositoryRoot: repoRoot,
|
|
1512
1511
|
config
|
|
1513
|
-
}, shared)
|
|
1512
|
+
}, shared));
|
|
1513
|
+
if (!result.subchecks?.length) return { skill: skillName, ...result };
|
|
1514
|
+
const checks = result.subchecks.map(normalizeVerificationRecord);
|
|
1515
|
+
const gate = summarizeGate(checks);
|
|
1516
|
+
return {
|
|
1517
|
+
skill: skillName,
|
|
1518
|
+
...result,
|
|
1519
|
+
status: gate,
|
|
1520
|
+
effectiveStatus: gate,
|
|
1521
|
+
reason: gate === 'pass' ? 'OK' : checks.find((check: any) => check.effectiveStatus !== 'pass')?.reason ?? 'CHECK_FAILED',
|
|
1522
|
+
action: buildAction(gate, checks),
|
|
1523
|
+
checks
|
|
1514
1524
|
};
|
|
1515
1525
|
}
|
|
1516
1526
|
|
|
@@ -20,6 +20,13 @@ type VerificationCheckResult = {
|
|
|
20
20
|
message: string;
|
|
21
21
|
fail_type?: string;
|
|
22
22
|
warnings?: string[];
|
|
23
|
+
checkId?: string;
|
|
24
|
+
classification?: 'hard' | 'soft' | 'info';
|
|
25
|
+
effectiveStatus?: VerificationStatus;
|
|
26
|
+
reason?: string;
|
|
27
|
+
action?: string;
|
|
28
|
+
dependsOn?: string;
|
|
29
|
+
subchecks?: VerificationCheckResult[];
|
|
23
30
|
};
|
|
24
31
|
type VerificationContext = {
|
|
25
32
|
skillName: string;
|
package/lib/task/verification.ts
CHANGED
|
@@ -126,7 +126,7 @@ async function verifyTaskEvent(request: { taskRef: string; event: string; artifa
|
|
|
126
126
|
} else {
|
|
127
127
|
for (const check of spec.checks ?? []) {
|
|
128
128
|
const payload = await engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
|
|
129
|
-
const status = payload.status as 'pass' | 'fail' | 'blocked';
|
|
129
|
+
const status = (payload.effectiveStatus ?? payload.status) as 'pass' | 'fail' | 'blocked';
|
|
130
130
|
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
131
131
|
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
|
|
132
132
|
break;
|
|
@@ -150,18 +150,34 @@ function statusLabel(status: unknown): string {
|
|
|
150
150
|
function renderPayload(payload: Record<string, unknown>): string {
|
|
151
151
|
const lines: string[] = [];
|
|
152
152
|
if (Array.isArray(payload.checks)) {
|
|
153
|
-
|
|
153
|
+
const gateMode = typeof payload.gate === 'string';
|
|
154
|
+
lines.push(gateMode
|
|
155
|
+
? `Verification: ${payload.gate} | Skill: ${payload.skill}`
|
|
156
|
+
: `Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
|
|
154
157
|
for (const check of payload.checks as Array<Record<string, unknown>>) {
|
|
155
|
-
|
|
158
|
+
const raw = statusLabel(check.status);
|
|
159
|
+
const effective = statusLabel(check.effectiveStatus ?? check.status);
|
|
160
|
+
const reason = check.reason ?? 'CHECK_FAILED';
|
|
161
|
+
const message = check.message ?? reason;
|
|
162
|
+
lines.push(` [${effective}] ${check.checkId ?? check.type} (${check.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${message}; ${check.action ?? 'Review validation output'}`);
|
|
156
163
|
}
|
|
157
|
-
|
|
164
|
+
const summary = payload.summary ?? (() => {
|
|
165
|
+
const checks = payload.checks as Array<Record<string, unknown>>;
|
|
166
|
+
const statuses = checks.map((check) => check.effectiveStatus ?? check.status);
|
|
167
|
+
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` : ''}`;
|
|
168
|
+
})();
|
|
169
|
+
lines.push('', `Result: ${summary} - ${payload.action}`);
|
|
158
170
|
} else {
|
|
159
171
|
lines.push(`Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
const raw = statusLabel(payload.status);
|
|
173
|
+
const effective = statusLabel(payload.effectiveStatus ?? payload.status);
|
|
174
|
+
const reason = payload.reason ?? 'CHECK_FAILED';
|
|
175
|
+
lines.push(` [${effective}] ${payload.checkId ?? payload.type} (${payload.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${payload.message ?? reason}; ${payload.action ?? 'Review validation output'}`);
|
|
176
|
+
const status = payload.effectiveStatus ?? payload.status;
|
|
177
|
+
const summary = status === 'pass' ? '1 passed, 0 failed'
|
|
178
|
+
: status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
|
|
179
|
+
const action = status === 'pass' ? 'Requested check passed'
|
|
180
|
+
: status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
|
|
165
181
|
: `Fix ${payload.type} issues and re-run check`;
|
|
166
182
|
lines.push('', `Result: ${summary} - ${action}`);
|
|
167
183
|
}
|
|
@@ -9,10 +9,13 @@ export const TASK_WORKFLOW_COMMANDS = Object.freeze({
|
|
|
9
9
|
'ledger-finding-response': ['task-ledger', 'finding-respond'],
|
|
10
10
|
'ledger-finding-review': ['task-ledger', 'finding-review'],
|
|
11
11
|
'ledger-finding-upsert': ['task-ledger', 'finding-upsert'],
|
|
12
|
+
'ledger-stage-status': ['task-ledger', 'stage-status'],
|
|
12
13
|
'decision-next-id': ['task-ledger', 'decision-next-id'],
|
|
13
14
|
'decision-upsert': ['task-ledger', 'decision-upsert'],
|
|
14
15
|
'invalidation-reconcile': ['task-invalidation', 'reconcile'],
|
|
15
|
-
'warning-add': ['task-warning', 'add']
|
|
16
|
+
'warning-add': ['task-warning', 'add'],
|
|
17
|
+
'warning-list': ['task-warning', 'list'],
|
|
18
|
+
'warning-set-status': ['task-warning', 'set-status']
|
|
16
19
|
} as const);
|
|
17
20
|
export type TaskWorkflowOperation = keyof typeof TASK_WORKFLOW_COMMANDS;
|
|
18
21
|
export const TASK_WORKFLOW_OPERATIONS = Object.freeze(Object.keys(TASK_WORKFLOW_COMMANDS) as TaskWorkflowOperation[]);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
1
3
|
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
2
4
|
import { applyLedgerIntent } from './ledger-intents.ts';
|
|
3
5
|
import type { LedgerIntent } from './ledger-intents.ts';
|
|
6
|
+
import { isReviewStage, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
7
|
+
import { resolveTaskRef } from './resolve-ref.ts';
|
|
4
8
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
5
9
|
import type { TaskWorkflowOperation } from './workflow-command.ts';
|
|
6
10
|
import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
|
|
@@ -116,7 +120,24 @@ function ledgerIntent(operation: TaskWorkflowOperation, args: readonly string[])
|
|
|
116
120
|
|
|
117
121
|
function warningIntent(args: readonly string[]): WorkflowWarningIntent | DispatchFailure {
|
|
118
122
|
const [taskRef, kind] = args;
|
|
119
|
-
if (!taskRef ||
|
|
123
|
+
if (!taskRef || !['add', 'list', 'set-status'].includes(kind ?? '')) {
|
|
124
|
+
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
|
|
125
|
+
}
|
|
126
|
+
if (kind === 'list') {
|
|
127
|
+
const values = options(args, { '--status': 'status' });
|
|
128
|
+
if (isFailure(values)) return values;
|
|
129
|
+
return { kind, taskRef, ...(value(values, '--status') ? { status: value(values, '--status') as never } : {}) };
|
|
130
|
+
}
|
|
131
|
+
if (kind === 'set-status') {
|
|
132
|
+
const values = options(args, { '--id': 'id', '--status': 'status', '--resolution': 'resolution', '--dry-run': 'dryRun' });
|
|
133
|
+
if (isFailure(values)) return values;
|
|
134
|
+
const missing = required(values, ['--id', '--status', '--resolution']);
|
|
135
|
+
if (missing) return missing;
|
|
136
|
+
return {
|
|
137
|
+
kind, taskRef, id: value(values, '--id')!, status: value(values, '--status') as never,
|
|
138
|
+
resolution: value(values, '--resolution')!, ...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
139
|
+
};
|
|
140
|
+
}
|
|
120
141
|
const values = options(args, {
|
|
121
142
|
'--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
|
|
122
143
|
'--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
|
|
@@ -131,6 +152,26 @@ function warningIntent(args: readonly string[]): WorkflowWarningIntent | Dispatc
|
|
|
131
152
|
} as WorkflowWarningIntent;
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
function stageStatus(repoRoot: string, args: readonly string[]): WorkflowDispatchResult {
|
|
156
|
+
const [taskRef, kind] = args;
|
|
157
|
+
if (!taskRef || kind !== 'stage-status') return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
|
|
158
|
+
const values = options(args, { '--stage': 'stage' });
|
|
159
|
+
if (isFailure(values)) return values;
|
|
160
|
+
const stage = value(values, '--stage');
|
|
161
|
+
if (!stage || !isReviewStage(stage)) return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'stage-status requires a valid review stage');
|
|
162
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot });
|
|
163
|
+
if (!resolved.ok) return failed(resolved.code, resolved.message);
|
|
164
|
+
try {
|
|
165
|
+
const ledger = parseLedgerDocument(fs.readFileSync(resolved.taskMdPath, 'utf8'));
|
|
166
|
+
if (!ledger.present) return failed('LEDGER_SECTION_MISSING', 'review disagreement ledger section is missing');
|
|
167
|
+
const invalid = validateLedgerRows(ledger.rows);
|
|
168
|
+
if (invalid) return failed(invalid.code, invalid.message);
|
|
169
|
+
return { status: 'ready', changed: false, taskId: resolved.taskId, stageStatus: summarizeLedgerStage(ledger.rows, stage), error: null };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return failed('LEDGER_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
134
175
|
/**
|
|
135
176
|
* The caller has already selected and authorized the execution boundary.
|
|
136
177
|
* This function deliberately invokes task domains in that process: starting a
|
|
@@ -157,10 +198,12 @@ export function dispatchWorkflowCommand(
|
|
|
157
198
|
...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
158
199
|
}));
|
|
159
200
|
}
|
|
160
|
-
if (operation === '
|
|
201
|
+
if (operation === 'ledger-stage-status') return stageStatus(repoRoot, args);
|
|
202
|
+
if (operation === 'warning-add' || operation === 'warning-list' || operation === 'warning-set-status') {
|
|
161
203
|
const intent = warningIntent(args);
|
|
162
204
|
if (isFailure(intent)) return intent;
|
|
163
|
-
|
|
205
|
+
if (intent.kind === 'list') return applyWorkflowWarningIntent(intent, { repoRoot });
|
|
206
|
+
return withTaskExecutionLock(repoRoot, taskRef, `task-warning.${intent.kind}`, () => applyWorkflowWarningIntent(intent, { repoRoot }));
|
|
164
207
|
}
|
|
165
208
|
const intent = ledgerIntent(operation, args);
|
|
166
209
|
if (isFailure(intent)) return intent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"installer"
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@clack/prompts": "1.
|
|
55
|
+
"@clack/prompts": "1.8.1",
|
|
56
56
|
"@larksuiteoapi/node-sdk": "^1.68.0",
|
|
57
57
|
"cross-spawn": "^7.0.6",
|
|
58
58
|
"fs-native-extensions": "1.5.1",
|