@fitlab-ai/agent-infra 0.11.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/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/platform/issues.js +3 -4
- package/dist/lib/platform/verification-sync.js +142 -154
- package/dist/lib/sandbox/control/workflow-executor.js +7 -1
- 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/events.js +60 -43
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/local-artifact-finalization.js +7 -4
- package/dist/lib/task/orchestration.js +8 -6
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-finalization.js +3 -9
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/verification-sync.ts +159 -177
- package/lib/sandbox/control/workflow-executor.ts +11 -1
- 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/events.ts +49 -32
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/local-artifact-finalization.ts +6 -4
- package/lib/task/orchestration.ts +12 -7
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-finalization.ts +3 -9
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/package.json +1 -1
- 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/local-artifact-repair.en.md +11 -3
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +11 -3
- 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 +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/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.zh-CN.md +4 -4
- 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/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/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
|
@@ -2,7 +2,7 @@ import { TASK_WORKFLOW_COMMANDS } from '../../task/workflow-command.js';
|
|
|
2
2
|
import { parseArtifactCommand, executeArtifactCommand } from '../../task/artifact-command.js';
|
|
3
3
|
import { parseReviewCommand } from '../../task/review-command.js';
|
|
4
4
|
import { prepareLocalArtifact, preflightLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.js';
|
|
5
|
-
import { prepareReviewSummaryCandidate, commitReviewSummaryProvenance } from '../../task/review-finalization.js';
|
|
5
|
+
import { prepareReviewSummaryCandidate, commitReviewSummaryProvenance, preflightReviewSummary } from '../../task/review-finalization.js';
|
|
6
6
|
import { readArtifactRecoveryIntent } from '../../task/artifact-repair-intent.js';
|
|
7
7
|
import { withTaskExecutionLock } from '../../task/task-execution-lock.js';
|
|
8
8
|
import { dispatchWorkflowCommand } from '../../task/workflow-dispatch.js';
|
|
@@ -116,6 +116,12 @@ export async function executeTaskWorkflow(manifest, request, lifecycleRecoveryAt
|
|
|
116
116
|
if (input.overrideTicket)
|
|
117
117
|
throw new Error('TASK_WORKFLOW_OVERRIDE_UNSUPPORTED');
|
|
118
118
|
const family = `review-${input.stage}`;
|
|
119
|
+
if (request.operation === 'review-preflight') {
|
|
120
|
+
const result = preflightReviewSummary(input, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true });
|
|
121
|
+
recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
|
|
122
|
+
observeRecoveryPublication();
|
|
123
|
+
return response(result);
|
|
124
|
+
}
|
|
119
125
|
recoveryObservation = {
|
|
120
126
|
family,
|
|
121
127
|
artifact: input.artifact,
|
|
@@ -150,72 +150,35 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
|
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
|
|
153
|
-
let content;
|
|
154
|
-
try {
|
|
155
|
-
content = fs.readFileSync(review.path, 'utf8');
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
const parsed = parseReviewedInputReference(content, expectedFamily);
|
|
162
|
-
if (!parsed) {
|
|
163
|
-
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
const abs = path.join(taskDir, parsed.name);
|
|
167
153
|
try {
|
|
154
|
+
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
155
|
+
const receipt = receiptForOutput(taskContent, review.name);
|
|
156
|
+
const expectedEvent = reviewEventName(review.family);
|
|
157
|
+
if (!receipt || receipt.event !== expectedEvent)
|
|
158
|
+
throw new Error(`receipt for ${review.name} is missing or invalid`);
|
|
159
|
+
const input = parseArtifactName(receipt.input);
|
|
160
|
+
if (!input || input.family !== expectedFamily)
|
|
161
|
+
throw new Error(`receipt input '${receipt.input}' is not a canonical ${expectedFamily} artifact`);
|
|
162
|
+
const abs = path.join(taskDir, input.name);
|
|
168
163
|
const stat = fs.lstatSync(abs);
|
|
169
164
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
170
165
|
throw new Error('referenced input is not a regular file');
|
|
171
|
-
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
172
166
|
const invalidation = parseInvalidationDocument(taskContent);
|
|
173
167
|
if (!invalidation.ok)
|
|
174
168
|
throw new Error(invalidation.message);
|
|
175
|
-
if (isArtifactInvalidated(invalidation.document, expectedFamily,
|
|
176
|
-
throw new Error(`${
|
|
177
|
-
}
|
|
178
|
-
const receipt = receiptForOutput(taskContent, review.name);
|
|
179
|
-
if (!receipt)
|
|
180
|
-
throw new Error(`receipt for ${review.name} is missing`);
|
|
181
|
-
const expectedEvent = reviewEventName(review.family);
|
|
182
|
-
if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
|
|
183
|
-
throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
|
|
184
|
-
}
|
|
169
|
+
if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name))
|
|
170
|
+
throw new Error(`${input.name} is invalidated`);
|
|
185
171
|
const actualSha256 = sha256File(abs);
|
|
186
172
|
if (actualSha256 !== receipt.inputSha256) {
|
|
187
|
-
throw new Error(`${
|
|
173
|
+
throw new Error(`${input.name} content digest does not match receipt`);
|
|
188
174
|
}
|
|
189
|
-
return { ...
|
|
175
|
+
return { ...input, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
190
176
|
}
|
|
191
177
|
catch (error) {
|
|
192
|
-
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name,
|
|
178
|
+
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
|
|
193
179
|
return null;
|
|
194
180
|
}
|
|
195
181
|
}
|
|
196
|
-
function parseReviewedInputReference(content, expectedFamily) {
|
|
197
|
-
const lines = content.split(/\r?\n/);
|
|
198
|
-
const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
199
|
-
const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
|
|
200
|
-
const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
|
|
201
|
-
return candidates
|
|
202
|
-
.map((name) => parseArtifactName(name))
|
|
203
|
-
.find((item) => item?.family === expectedFamily) ?? null;
|
|
204
|
-
}
|
|
205
|
-
function parseCodePlanInputReference(content) {
|
|
206
|
-
const lines = content.split(/\r?\n/);
|
|
207
|
-
const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
|
|
208
|
-
if (sectionStart < 0)
|
|
209
|
-
return null;
|
|
210
|
-
const bodyLines = lines.slice(sectionStart + 1);
|
|
211
|
-
const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
|
|
212
|
-
const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
|
|
213
|
-
const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
|
|
214
|
-
if (!match)
|
|
215
|
-
return null;
|
|
216
|
-
const parsed = parseArtifactName(match[1]);
|
|
217
|
-
return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
|
|
218
|
-
}
|
|
219
182
|
function reviewEventName(family) {
|
|
220
183
|
if (family === 'review-analysis')
|
|
221
184
|
return 'review-analysis.completed';
|
|
@@ -509,5 +472,5 @@ function buildArtifactLinkSection(content, artifact) {
|
|
|
509
472
|
body = lines.join('\n').replace(/^\n+|\n+$/g, '');
|
|
510
473
|
return { aliases, heading, body };
|
|
511
474
|
}
|
|
512
|
-
export { artifactFamilyCatalog, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
|
|
475
|
+
export { artifactFamilyCatalog, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, resolveCodePlanInput, reviewEventName, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
|
|
513
476
|
//# sourceMappingURL=artifact-lifecycle.js.map
|
|
@@ -6,6 +6,7 @@ import { parseArtifactName } from './artifact-name.js';
|
|
|
6
6
|
import { scanVisibleMarkdown } from './markdown.js';
|
|
7
7
|
import { getArtifactSchema, renderArtifactSkeleton } from './artifact-schema.js';
|
|
8
8
|
import { withTaskExecutionLock } from './task-execution-lock.js';
|
|
9
|
+
import { buildQualificationAudit, expectedQualificationRelations, parseTaskQualification, renderQualificationAudit } from './qualification-audit.js';
|
|
9
10
|
function sha256Content(content) {
|
|
10
11
|
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
11
12
|
}
|
|
@@ -193,6 +194,27 @@ function artifactRound(family, artifact) {
|
|
|
193
194
|
const identity = parseArtifactName(artifact);
|
|
194
195
|
return identity?.family === family ? identity.round : null;
|
|
195
196
|
}
|
|
197
|
+
function qualificationAuditForInit(request) {
|
|
198
|
+
let taskContent;
|
|
199
|
+
try {
|
|
200
|
+
taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
|
|
204
|
+
}
|
|
205
|
+
const qualification = parseTaskQualification(taskContent);
|
|
206
|
+
if (!qualification.ok)
|
|
207
|
+
return qualification;
|
|
208
|
+
if (!qualification.qualification.present)
|
|
209
|
+
return { ok: true, content: '' };
|
|
210
|
+
const relations = expectedQualificationRelations(taskContent, request.family);
|
|
211
|
+
if (!relations.ok)
|
|
212
|
+
return relations;
|
|
213
|
+
const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
|
|
214
|
+
if (!audit.ok)
|
|
215
|
+
return audit;
|
|
216
|
+
return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
|
|
217
|
+
}
|
|
196
218
|
function initializeArtifactSkeleton(request) {
|
|
197
219
|
const schema = getArtifactSchema(request.family);
|
|
198
220
|
if (!schema)
|
|
@@ -216,12 +238,16 @@ function initializeArtifactSkeleton(request) {
|
|
|
216
238
|
if (error.code !== 'ENOENT')
|
|
217
239
|
return resultFailure('ARTIFACT_INIT_TARGET_INVALID', String(error));
|
|
218
240
|
}
|
|
219
|
-
const
|
|
241
|
+
const skeleton = renderArtifactSkeleton({
|
|
220
242
|
taskId: request.taskId,
|
|
221
243
|
family: request.family,
|
|
222
244
|
artifact: request.artifact,
|
|
223
245
|
...(request.locale ? { locale: request.locale } : {})
|
|
224
246
|
});
|
|
247
|
+
const audit = qualificationAuditForInit(request);
|
|
248
|
+
if (!audit.ok)
|
|
249
|
+
return resultFailure(audit.code, audit.message);
|
|
250
|
+
const content = `${skeleton.trimEnd()}${audit.content}`;
|
|
225
251
|
const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
|
|
226
252
|
try {
|
|
227
253
|
fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
@@ -146,8 +146,6 @@ function qualificationRecoveryAction(facts) {
|
|
|
146
146
|
return QUALIFICATION_RECOVERY_ORDER.find(({ pattern }) => stale.some((name) => pattern.test(name)))?.action ?? null;
|
|
147
147
|
}
|
|
148
148
|
function recommendNext(facts) {
|
|
149
|
-
if (invalidationBlocks(facts.invalidation))
|
|
150
|
-
return { action: null, reasonCode: 'INVALIDATION_INCOMPLETE', evidence: ['reconcile task invalidation before routing'] };
|
|
151
149
|
if (facts.qualificationStale) {
|
|
152
150
|
const action = qualificationRecoveryAction(facts);
|
|
153
151
|
return {
|
package/dist/lib/task/events.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
3
4
|
import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
|
|
4
|
-
import { buildArtifactLinkSection, inspectArtifactDirectory,
|
|
5
|
+
import { buildArtifactLinkSection, inspectArtifactDirectory, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
5
6
|
import { artifactName, parseArtifactName } from './artifact-name.js';
|
|
6
7
|
import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
|
|
7
8
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
8
9
|
import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
9
10
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
10
11
|
import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.js';
|
|
12
|
+
import { extractReviewBaseline, extractReviewedHead, extractReviewedSnapshotTree } from './review-fingerprint.js';
|
|
11
13
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
12
14
|
import { findSectionHeading } from './sections.js';
|
|
13
15
|
import { validateLifecycleExecution } from './lifecycle-execution.js';
|
|
@@ -20,6 +22,7 @@ import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artif
|
|
|
20
22
|
import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
|
|
21
23
|
import { buildLifecycleFacts, canStart } from './capabilities.js';
|
|
22
24
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
|
|
25
|
+
import { reconcileTaskInvalidation } from './invalidation-command.js';
|
|
23
26
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
|
|
24
27
|
import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.js';
|
|
25
28
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
@@ -36,6 +39,27 @@ const eventCatalog = [
|
|
|
36
39
|
'manual-validation.started', 'manual-validation.completed',
|
|
37
40
|
'validation-run.started', 'validation-run.completed'
|
|
38
41
|
];
|
|
42
|
+
function approvedCleanReviewedCommit(reviewContent, verdict, repoRoot) {
|
|
43
|
+
if (verdict !== 'approved')
|
|
44
|
+
return null;
|
|
45
|
+
const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
|
|
46
|
+
const reviewedTree = extractReviewedSnapshotTree(reviewContent);
|
|
47
|
+
if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const currentHead = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
51
|
+
if (currentHead !== reviewedHead)
|
|
52
|
+
return null;
|
|
53
|
+
const currentTree = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
|
|
54
|
+
if (currentTree !== reviewedTree)
|
|
55
|
+
return null;
|
|
56
|
+
execFileSync('git', ['-C', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
|
|
57
|
+
return reviewedHead;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
39
63
|
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
|
|
40
64
|
const SCHEMAS = {
|
|
41
65
|
'analyze.started': { optional: ['round'] },
|
|
@@ -345,9 +369,7 @@ function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, arti
|
|
|
345
369
|
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
346
370
|
}
|
|
347
371
|
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
348
|
-
const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
|
|
349
372
|
if (intent.round !== round
|
|
350
|
-
|| intent.requestId !== expectedRequestId
|
|
351
373
|
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
352
374
|
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
353
375
|
return {
|
|
@@ -595,34 +617,22 @@ function reworkIntentMutationForCompletion(content, taskDir, family, artifact, t
|
|
|
595
617
|
}
|
|
596
618
|
function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
|
|
597
619
|
if (family === 'code') {
|
|
598
|
-
let content;
|
|
599
|
-
try {
|
|
600
|
-
content = fs.readFileSync(artifact.path, 'utf8');
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
604
|
-
}
|
|
605
|
-
const input = parseCodePlanInputReference(content);
|
|
606
|
-
if (!input)
|
|
607
|
-
return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
608
620
|
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
609
621
|
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
610
622
|
if (!startedInput || !startedSha256)
|
|
611
623
|
return { ok: false, message: 'code.started plan input context is missing' };
|
|
612
|
-
if (input.name !== startedInput)
|
|
613
|
-
return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
614
624
|
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
615
|
-
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !==
|
|
616
|
-
return { ok: false, message: `code input '${
|
|
625
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
|
|
626
|
+
return { ok: false, message: `code input '${startedInput}' is not the latest plan artifact` };
|
|
617
627
|
}
|
|
618
628
|
try {
|
|
619
629
|
const inputSha256 = sha256File(plan.latest.path);
|
|
620
630
|
if (inputSha256 !== startedSha256)
|
|
621
|
-
return { ok: false, message: `code input ${
|
|
631
|
+
return { ok: false, message: `code input ${startedInput} changed after code.started` };
|
|
622
632
|
return {
|
|
623
633
|
ok: true,
|
|
624
634
|
receipt: {
|
|
625
|
-
event: 'code.completed', output: artifact.name, input:
|
|
635
|
+
event: 'code.completed', output: artifact.name, input: startedInput,
|
|
626
636
|
inputSha256, completedAt
|
|
627
637
|
}
|
|
628
638
|
};
|
|
@@ -634,25 +644,13 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
|
|
|
634
644
|
if (!family.startsWith('review-'))
|
|
635
645
|
return null;
|
|
636
646
|
const expectedFamily = reviewInputFamily(family);
|
|
637
|
-
let content;
|
|
638
|
-
try {
|
|
639
|
-
content = fs.readFileSync(artifact.path, 'utf8');
|
|
640
|
-
}
|
|
641
|
-
catch (error) {
|
|
642
|
-
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
643
|
-
}
|
|
644
|
-
const input = parseReviewedInputReference(content, expectedFamily);
|
|
645
|
-
if (!input)
|
|
646
|
-
return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
647
647
|
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
648
648
|
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
649
649
|
if (!startedInput || !startedSha256)
|
|
650
650
|
return { ok: false, message: 'review started input context is missing' };
|
|
651
|
-
if (input.name !== startedInput)
|
|
652
|
-
return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
653
651
|
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
654
|
-
if (current.status !== 'ready' || !current.latest || current.latest.name !==
|
|
655
|
-
return { ok: false, message: `review input '${
|
|
652
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
|
|
653
|
+
return { ok: false, message: `review input '${startedInput}' is not the latest ${expectedFamily} artifact` };
|
|
656
654
|
}
|
|
657
655
|
const event = family === 'review-analysis'
|
|
658
656
|
? 'review-analysis.completed'
|
|
@@ -663,17 +661,17 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
|
|
|
663
661
|
throw new Error('review input is not a regular file');
|
|
664
662
|
const inputSha256 = sha256File(current.latest.path);
|
|
665
663
|
if (inputSha256 !== startedSha256)
|
|
666
|
-
return { ok: false, message: `review input ${
|
|
664
|
+
return { ok: false, message: `review input ${startedInput} changed after review.started` };
|
|
667
665
|
return {
|
|
668
666
|
ok: true,
|
|
669
667
|
receipt: {
|
|
670
|
-
event, output: artifact.name, input:
|
|
668
|
+
event, output: artifact.name, input: startedInput,
|
|
671
669
|
inputSha256, completedAt
|
|
672
670
|
}
|
|
673
671
|
};
|
|
674
672
|
}
|
|
675
673
|
catch (error) {
|
|
676
|
-
return { ok: false, message: `cannot hash review input ${
|
|
674
|
+
return { ok: false, message: `cannot hash review input ${startedInput}: ${error instanceof Error ? error.message : String(error)}` };
|
|
677
675
|
}
|
|
678
676
|
}
|
|
679
677
|
function applyTaskEventUnlocked(request, options = {}) {
|
|
@@ -693,6 +691,24 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
693
691
|
catch (error) {
|
|
694
692
|
return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
695
693
|
}
|
|
694
|
+
const initialParts = eventParts(request.event);
|
|
695
|
+
if (initialParts.phase === 'started' && !request.dryRun) {
|
|
696
|
+
const reconciled = reconcileTaskInvalidation(request.taskRef, { repoRoot: resolved.repoRoot });
|
|
697
|
+
if (reconciled.status === 'failed') {
|
|
698
|
+
return failed(request, {
|
|
699
|
+
code: 'EVENT_TRANSITION_INVALID',
|
|
700
|
+
message: `INVALIDATION_RECONCILE_FAILED: ${reconciled.error?.code ?? 'UNKNOWN'}: ${reconciled.error?.message ?? ''}`
|
|
701
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
702
|
+
}
|
|
703
|
+
if (reconciled.changed) {
|
|
704
|
+
try {
|
|
705
|
+
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
696
712
|
let frontmatter;
|
|
697
713
|
try {
|
|
698
714
|
frontmatter = parseTypedTaskFrontmatter(content);
|
|
@@ -706,7 +722,6 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
706
722
|
const rows = startedBackedRows(pairEntries(section.entries));
|
|
707
723
|
let normalized = request;
|
|
708
724
|
let artifactContext = null;
|
|
709
|
-
const initialParts = eventParts(request.event);
|
|
710
725
|
if (initialParts.phase === 'started') {
|
|
711
726
|
const openIdentity = openStartedIdentity(rows, initialParts.family);
|
|
712
727
|
if (openIdentity && 'conflict' in openIdentity)
|
|
@@ -941,14 +956,9 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
941
956
|
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
942
957
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
943
958
|
}
|
|
944
|
-
const reviewStage = eventIdentity.family === 'review-analysis'
|
|
945
|
-
? 'analysis'
|
|
946
|
-
: eventIdentity.family === 'review-plan' ? 'plan' : 'code';
|
|
947
959
|
const actualSha256 = sha256File(completedArtifact.path);
|
|
948
960
|
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
949
|
-
const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
|
|
950
961
|
if (reviewFinalizationIntent.round !== normalized.round
|
|
951
|
-
|| reviewFinalizationIntent.requestId !== expectedRequestId
|
|
952
962
|
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
953
963
|
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
954
964
|
return failed(normalized, {
|
|
@@ -1060,6 +1070,13 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1060
1070
|
else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
1061
1071
|
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
1062
1072
|
}
|
|
1073
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
|
|
1074
|
+
const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot);
|
|
1075
|
+
if (reviewedCommit)
|
|
1076
|
+
frontmatterSet.last_reviewed_commit = reviewedCommit;
|
|
1077
|
+
else if (normalized.verdict === 'approved')
|
|
1078
|
+
frontmatterSet.last_reviewed_commit = '';
|
|
1079
|
+
}
|
|
1063
1080
|
if (eventIdentity.phase === 'completed')
|
|
1064
1081
|
frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
|
|
1065
1082
|
if (eventIdentity.phase === 'started' && normalized.implementationInput)
|
|
@@ -1230,7 +1247,7 @@ function applyTaskEvent(request, options = {}) {
|
|
|
1230
1247
|
const parts = eventParts(request.event);
|
|
1231
1248
|
if (options.lockAlreadyHeld)
|
|
1232
1249
|
return applyTaskEventUnlocked(request, options);
|
|
1233
|
-
if (request.dryRun ||
|
|
1250
|
+
if (request.dryRun || !['started', 'completed'].includes(parts.phase)) {
|
|
1234
1251
|
return applyTaskEventUnlocked(request, options);
|
|
1235
1252
|
}
|
|
1236
1253
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const OPTIONAL_PLATFORM_AUDITS = new Set([
|
|
2
|
+
'closed-status-labels', 'status-label', 'comment-marker', 'pr-comment-marker',
|
|
3
|
+
'pr-comment-last-commit', 'pr-comment-content', 'comment-content', 'task-comment-content',
|
|
4
|
+
'in-labels-computed', 'pr-type-label', 'in-labels-match-pr', 'pr-assignee',
|
|
5
|
+
'requirements', 'issue-type', 'issue-fields', 'milestone'
|
|
6
|
+
]);
|
|
7
|
+
const STATUS_LABELS = {
|
|
8
|
+
'create-task': 'status: waiting-for-triage',
|
|
9
|
+
'analyze-task': 'status: pending-design-work',
|
|
10
|
+
'review-analysis': 'status: pending-design-work',
|
|
11
|
+
'plan-task': 'status: pending-design-work',
|
|
12
|
+
'review-plan': 'status: pending-design-work',
|
|
13
|
+
'code-task': 'status: in-progress',
|
|
14
|
+
'review-code': 'status: in-progress'
|
|
15
|
+
};
|
|
16
|
+
const ARTIFACT_COMMENT_SKILLS = new Set([
|
|
17
|
+
'analyze-task', 'plan-task', 'review-analysis', 'review-plan', 'code-task',
|
|
18
|
+
'review-code', 'review-pr', 'run-manual-validation'
|
|
19
|
+
]);
|
|
20
|
+
const PR_AUDIT_SKILLS = new Set(['create-pr']);
|
|
21
|
+
const IN_LABEL_AUDIT_SKILLS = new Set(['code-task', 'commit', 'create-pr']);
|
|
22
|
+
export function platformAuditPolicy(checkId, context) {
|
|
23
|
+
const skillName = context.skillName || '';
|
|
24
|
+
const artifactComment = Boolean(context.artifactFile) && ARTIFACT_COMMENT_SKILLS.has(skillName);
|
|
25
|
+
const prAudit = PR_AUDIT_SKILLS.has(skillName);
|
|
26
|
+
const inLabelAudit = IN_LABEL_AUDIT_SKILLS.has(skillName);
|
|
27
|
+
switch (checkId) {
|
|
28
|
+
case 'status-label':
|
|
29
|
+
return { classification: 'soft', enabled: Boolean(STATUS_LABELS[skillName]), expectedStatusLabel: STATUS_LABELS[skillName] };
|
|
30
|
+
case 'comment-marker':
|
|
31
|
+
return {
|
|
32
|
+
classification: 'soft',
|
|
33
|
+
enabled: artifactComment || skillName === 'complete-task',
|
|
34
|
+
...(artifactComment ? { expectedCommentMarkerKey: 'artifact' } : skillName === 'complete-task' ? { expectedCommentMarkerKey: 'summary' } : {})
|
|
35
|
+
};
|
|
36
|
+
case 'pr-comment-marker':
|
|
37
|
+
return {
|
|
38
|
+
classification: 'soft',
|
|
39
|
+
enabled: skillName === 'commit' || prAudit,
|
|
40
|
+
...(skillName === 'commit' || prAudit ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
41
|
+
};
|
|
42
|
+
case 'pr-comment-last-commit':
|
|
43
|
+
return {
|
|
44
|
+
classification: 'soft',
|
|
45
|
+
enabled: skillName === 'commit',
|
|
46
|
+
...(skillName === 'commit' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
47
|
+
};
|
|
48
|
+
case 'pr-comment-content':
|
|
49
|
+
return {
|
|
50
|
+
classification: 'soft',
|
|
51
|
+
enabled: skillName === 'complete-manual-validation',
|
|
52
|
+
...(skillName === 'complete-manual-validation' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
53
|
+
};
|
|
54
|
+
case 'comment-content':
|
|
55
|
+
return { classification: 'soft', enabled: artifactComment, expectedCommentMarkerKey: 'artifact' };
|
|
56
|
+
case 'task-comment-content':
|
|
57
|
+
return { classification: 'soft', enabled: true };
|
|
58
|
+
case 'in-labels-computed':
|
|
59
|
+
return { classification: 'soft', enabled: inLabelAudit };
|
|
60
|
+
case 'pr-type-label':
|
|
61
|
+
case 'in-labels-match-pr':
|
|
62
|
+
case 'pr-assignee':
|
|
63
|
+
return { classification: 'soft', enabled: prAudit };
|
|
64
|
+
case 'requirements':
|
|
65
|
+
return { classification: 'soft', enabled: skillName === 'complete-task' };
|
|
66
|
+
case 'milestone':
|
|
67
|
+
return { classification: 'soft', enabled: true, requireSpecificMilestone: skillName === 'code-task' };
|
|
68
|
+
case 'closed-status-labels':
|
|
69
|
+
case 'issue-type':
|
|
70
|
+
case 'issue-fields':
|
|
71
|
+
return { classification: 'soft', enabled: true };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function classificationForCheck(checkId) {
|
|
75
|
+
return checkId.startsWith('platform.') && OPTIONAL_PLATFORM_AUDITS.has(checkId.slice('platform.'.length))
|
|
76
|
+
? 'soft'
|
|
77
|
+
: 'hard';
|
|
78
|
+
}
|
|
79
|
+
export function effectiveStatus(status, classification) {
|
|
80
|
+
return classification === 'hard' ? status : 'pass';
|
|
81
|
+
}
|
|
82
|
+
export function normalizeVerificationRecord(record) {
|
|
83
|
+
const checkId = record.checkId ?? record.type;
|
|
84
|
+
const classification = record.classification ?? classificationForCheck(checkId);
|
|
85
|
+
return {
|
|
86
|
+
...record,
|
|
87
|
+
checkId,
|
|
88
|
+
classification,
|
|
89
|
+
effectiveStatus: record.effectiveStatus ?? effectiveStatus(record.status, classification),
|
|
90
|
+
reason: record.reason ?? record.fail_type ?? (classification === 'info' ? 'NOT_APPLICABLE' : record.status === 'pass' ? 'OK' : 'CHECK_FAILED'),
|
|
91
|
+
action: record.action ?? (classification === 'info' || record.status === 'pass' ? 'No action required' : 'Review the check evidence and retry after correction')
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=gate-policy.js.map
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { inspectDecisionDetailDuplicates } from './decision-details.js';
|
|
3
3
|
import { scanVisibleMarkdown } from './markdown.js';
|
|
4
4
|
import { parseArtifactName } from './artifact-name.js';
|
|
5
|
-
import { hasOpenArtifactRound,
|
|
5
|
+
import { hasOpenArtifactRound, validateArtifactPublication, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
6
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
7
7
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
8
8
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.js';
|
|
@@ -97,13 +97,16 @@ function validateLocalArtifact(content, options) {
|
|
|
97
97
|
if (!decisionDetails.ok)
|
|
98
98
|
diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
99
99
|
if (options.taskContent !== undefined) {
|
|
100
|
-
const
|
|
101
|
-
|
|
100
|
+
const identity = options.artifact ? parseArtifactName(options.artifact) : null;
|
|
101
|
+
const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
|
|
102
|
+
? expectedQualificationRelations(options.taskContent, options.family)
|
|
103
|
+
: undefined;
|
|
104
|
+
if (expected && !expected.ok)
|
|
102
105
|
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
103
106
|
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
104
107
|
family: options.family,
|
|
105
108
|
artifact: options.artifact,
|
|
106
|
-
expectedUpstreamRelations: expected
|
|
109
|
+
expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
|
|
107
110
|
});
|
|
108
111
|
if (!qualification.ok)
|
|
109
112
|
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
@@ -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))
|