@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
|
@@ -3,7 +3,11 @@ import { TASK_WORKFLOW_COMMANDS } from '../../task/workflow-command.ts';
|
|
|
3
3
|
import { parseArtifactCommand, executeArtifactCommand } from '../../task/artifact-command.ts';
|
|
4
4
|
import { parseReviewCommand } from '../../task/review-command.ts';
|
|
5
5
|
import { prepareLocalArtifact, preflightLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.ts';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
prepareReviewSummaryCandidate,
|
|
8
|
+
commitReviewSummaryProvenance,
|
|
9
|
+
preflightReviewSummary
|
|
10
|
+
} from '../../task/review-finalization.ts';
|
|
7
11
|
import { readArtifactRecoveryIntent } from '../../task/artifact-repair-intent.ts';
|
|
8
12
|
import { withTaskExecutionLock } from '../../task/task-execution-lock.ts';
|
|
9
13
|
import { dispatchWorkflowCommand } from '../../task/workflow-dispatch.ts';
|
|
@@ -143,6 +147,12 @@ export async function executeTaskWorkflow(
|
|
|
143
147
|
} else {
|
|
144
148
|
if (input.overrideTicket) throw new Error('TASK_WORKFLOW_OVERRIDE_UNSUPPORTED');
|
|
145
149
|
const family = `review-${input.stage}` as 'review-analysis' | 'review-plan' | 'review-code';
|
|
150
|
+
if (request.operation === 'review-preflight') {
|
|
151
|
+
const result = preflightReviewSummary(input, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true });
|
|
152
|
+
recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
|
|
153
|
+
observeRecoveryPublication();
|
|
154
|
+
return response(result);
|
|
155
|
+
}
|
|
146
156
|
recoveryObservation = {
|
|
147
157
|
family,
|
|
148
158
|
artifact: input.artifact,
|
|
@@ -221,67 +221,30 @@ function resolveReviewedInput(
|
|
|
221
221
|
expectedFamily: ArtifactFamily,
|
|
222
222
|
diagnostics: ArtifactDiagnostic[]
|
|
223
223
|
): ArtifactIdentity | null {
|
|
224
|
-
let content: string;
|
|
225
|
-
try { content = fs.readFileSync(review.path, 'utf8'); }
|
|
226
|
-
catch (error) {
|
|
227
|
-
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
const parsed = parseReviewedInputReference(content, expectedFamily);
|
|
231
|
-
if (!parsed) {
|
|
232
|
-
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
const abs = path.join(taskDir, parsed.name);
|
|
236
224
|
try {
|
|
225
|
+
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
226
|
+
const receipt = receiptForOutput(taskContent, review.name);
|
|
227
|
+
const expectedEvent = reviewEventName(review.family);
|
|
228
|
+
if (!receipt || receipt.event !== expectedEvent) throw new Error(`receipt for ${review.name} is missing or invalid`);
|
|
229
|
+
const input = parseArtifactName(receipt.input);
|
|
230
|
+
if (!input || input.family !== expectedFamily) throw new Error(`receipt input '${receipt.input}' is not a canonical ${expectedFamily} artifact`);
|
|
231
|
+
const abs = path.join(taskDir, input.name);
|
|
237
232
|
const stat = fs.lstatSync(abs);
|
|
238
233
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
|
|
239
|
-
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
240
234
|
const invalidation = parseInvalidationDocument(taskContent);
|
|
241
235
|
if (!invalidation.ok) throw new Error(invalidation.message);
|
|
242
|
-
if (isArtifactInvalidated(invalidation.document, expectedFamily,
|
|
243
|
-
throw new Error(`${parsed.name} is invalidated`);
|
|
244
|
-
}
|
|
245
|
-
const receipt = receiptForOutput(taskContent, review.name);
|
|
246
|
-
if (!receipt) throw new Error(`receipt for ${review.name} is missing`);
|
|
247
|
-
const expectedEvent = reviewEventName(review.family);
|
|
248
|
-
if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
|
|
249
|
-
throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
|
|
250
|
-
}
|
|
236
|
+
if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name)) throw new Error(`${input.name} is invalidated`);
|
|
251
237
|
const actualSha256 = sha256File(abs);
|
|
252
238
|
if (actualSha256 !== receipt.inputSha256) {
|
|
253
|
-
throw new Error(`${
|
|
239
|
+
throw new Error(`${input.name} content digest does not match receipt`);
|
|
254
240
|
}
|
|
255
|
-
return { ...
|
|
241
|
+
return { ...input, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
256
242
|
} catch (error) {
|
|
257
|
-
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name,
|
|
243
|
+
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
|
|
258
244
|
return null;
|
|
259
245
|
}
|
|
260
246
|
}
|
|
261
247
|
|
|
262
|
-
function parseReviewedInputReference(content: string, expectedFamily: ArtifactFamily): { family: ArtifactFamily; round: number; name: string } | null {
|
|
263
|
-
const lines = content.split(/\r?\n/);
|
|
264
|
-
const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
265
|
-
const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
|
|
266
|
-
const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
|
|
267
|
-
return candidates
|
|
268
|
-
.map((name) => parseArtifactName(name))
|
|
269
|
-
.find((item) => item?.family === expectedFamily) ?? null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function parseCodePlanInputReference(content: string): { family: 'plan'; round: number; name: string } | null {
|
|
273
|
-
const lines = content.split(/\r?\n/);
|
|
274
|
-
const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
|
|
275
|
-
if (sectionStart < 0) return null;
|
|
276
|
-
const bodyLines = lines.slice(sectionStart + 1);
|
|
277
|
-
const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
|
|
278
|
-
const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
|
|
279
|
-
const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
|
|
280
|
-
if (!match) return null;
|
|
281
|
-
const parsed = parseArtifactName(match[1]!);
|
|
282
|
-
return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
248
|
function reviewEventName(family: ArtifactFamily): 'review-analysis.completed' | 'review-plan.completed' | 'review-code.completed' {
|
|
286
249
|
if (family === 'review-analysis') return 'review-analysis.completed';
|
|
287
250
|
if (family === 'review-plan') return 'review-plan.completed';
|
|
@@ -557,7 +520,7 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
|
|
|
557
520
|
export {
|
|
558
521
|
artifactFamilyCatalog, familySpec,
|
|
559
522
|
inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
|
|
560
|
-
|
|
523
|
+
resolveCodePlanInput, reviewEventName,
|
|
561
524
|
assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection
|
|
562
525
|
};
|
|
563
526
|
export type {
|
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
renderArtifactSkeleton
|
|
11
11
|
} from './artifact-schema.ts';
|
|
12
12
|
import { withTaskExecutionLock } from './task-execution-lock.ts';
|
|
13
|
+
import {
|
|
14
|
+
buildQualificationAudit,
|
|
15
|
+
expectedQualificationRelations,
|
|
16
|
+
parseTaskQualification,
|
|
17
|
+
renderQualificationAudit
|
|
18
|
+
} from './qualification-audit.ts';
|
|
13
19
|
import type {
|
|
14
20
|
ArtifactSchema,
|
|
15
21
|
ArtifactSchemaFamily,
|
|
@@ -288,6 +294,23 @@ function artifactRound(family: ArtifactSchemaFamily, artifact: string): number |
|
|
|
288
294
|
return identity?.family === family ? identity.round : null;
|
|
289
295
|
}
|
|
290
296
|
|
|
297
|
+
function qualificationAuditForInit(request: ArtifactInitRequest): { ok: true; content: string } | { ok: false; code: string; message: string } {
|
|
298
|
+
let taskContent: string;
|
|
299
|
+
try {
|
|
300
|
+
taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
|
|
303
|
+
}
|
|
304
|
+
const qualification = parseTaskQualification(taskContent);
|
|
305
|
+
if (!qualification.ok) return qualification;
|
|
306
|
+
if (!qualification.qualification.present) return { ok: true, content: '' };
|
|
307
|
+
const relations = expectedQualificationRelations(taskContent, request.family);
|
|
308
|
+
if (!relations.ok) return relations;
|
|
309
|
+
const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
|
|
310
|
+
if (!audit.ok) return audit;
|
|
311
|
+
return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
|
|
312
|
+
}
|
|
313
|
+
|
|
291
314
|
function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
|
|
292
315
|
const schema = getArtifactSchema(request.family);
|
|
293
316
|
if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
|
|
@@ -304,12 +327,15 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
|
|
|
304
327
|
} catch (error) {
|
|
305
328
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return resultFailure('ARTIFACT_INIT_TARGET_INVALID', String(error));
|
|
306
329
|
}
|
|
307
|
-
const
|
|
330
|
+
const skeleton = renderArtifactSkeleton({
|
|
308
331
|
taskId: request.taskId,
|
|
309
332
|
family: request.family,
|
|
310
333
|
artifact: request.artifact,
|
|
311
334
|
...(request.locale ? { locale: request.locale } : {})
|
|
312
335
|
});
|
|
336
|
+
const audit = qualificationAuditForInit(request);
|
|
337
|
+
if (!audit.ok) return resultFailure(audit.code, audit.message);
|
|
338
|
+
const content = `${skeleton.trimEnd()}${audit.content}`;
|
|
313
339
|
const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
|
|
314
340
|
try {
|
|
315
341
|
fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
package/lib/task/capabilities.ts
CHANGED
|
@@ -187,7 +187,6 @@ function qualificationRecoveryAction(facts: LifecycleFacts): LifecycleAction | n
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
190
|
-
if (invalidationBlocks(facts.invalidation)) return { action: null, reasonCode: 'INVALIDATION_INCOMPLETE', evidence: ['reconcile task invalidation before routing'] };
|
|
191
190
|
if (facts.qualificationStale) {
|
|
192
191
|
const action = qualificationRecoveryAction(facts);
|
|
193
192
|
return {
|
package/lib/task/events.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
3
4
|
|
|
4
5
|
import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
|
|
5
6
|
import {
|
|
6
7
|
buildArtifactLinkSection,
|
|
7
8
|
inspectArtifactDirectory,
|
|
8
|
-
parseReviewedInputReference,
|
|
9
|
-
parseCodePlanInputReference,
|
|
10
9
|
resolveArtifactContext,
|
|
11
10
|
validateCompletedArtifact
|
|
12
11
|
} from './artifact-lifecycle.ts';
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
25
24
|
import type { ReviewStage } from './ledger.ts';
|
|
26
25
|
import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
|
|
26
|
+
import { extractReviewBaseline, extractReviewedHead, extractReviewedSnapshotTree } from './review-fingerprint.ts';
|
|
27
27
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
28
28
|
import { findSectionHeading } from './sections.ts';
|
|
29
29
|
import { validateLifecycleExecution } from './lifecycle-execution.ts';
|
|
@@ -46,6 +46,7 @@ import { buildLifecycleFacts, canStart } from './capabilities.ts';
|
|
|
46
46
|
import type { ExplicitTrigger, LifecycleAction, TriggerInitiator, TriggerReason } from './capabilities.ts';
|
|
47
47
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.ts';
|
|
48
48
|
import type { InvalidationTargetKind } from './invalidation.ts';
|
|
49
|
+
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
49
50
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.ts';
|
|
50
51
|
import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.ts';
|
|
51
52
|
import type { QualificationAudit, UpstreamRelation } from './qualification-audit.ts';
|
|
@@ -113,6 +114,23 @@ type TaskEventResult = {
|
|
|
113
114
|
operations: readonly TaskOperationSummary[]; error: TaskEventError | null;
|
|
114
115
|
};
|
|
115
116
|
|
|
117
|
+
function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | undefined, repoRoot: string): string | null {
|
|
118
|
+
if (verdict !== 'approved') return null;
|
|
119
|
+
const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
|
|
120
|
+
const reviewedTree = extractReviewedSnapshotTree(reviewContent);
|
|
121
|
+
if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree)) return null;
|
|
122
|
+
try {
|
|
123
|
+
const currentHead = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
124
|
+
if (currentHead !== reviewedHead) return null;
|
|
125
|
+
const currentTree = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
|
|
126
|
+
if (currentTree !== reviewedTree) return null;
|
|
127
|
+
execFileSync('git', ['-C', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
|
|
128
|
+
return reviewedHead;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
116
134
|
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
|
|
117
135
|
const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
|
|
118
136
|
'analyze.started': { optional: ['round'] },
|
|
@@ -413,9 +431,7 @@ function reconcileReviewCompletionReplay(
|
|
|
413
431
|
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
414
432
|
}
|
|
415
433
|
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
416
|
-
const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
|
|
417
434
|
if (intent.round !== round
|
|
418
|
-
|| intent.requestId !== expectedRequestId
|
|
419
435
|
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
420
436
|
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
421
437
|
return {
|
|
@@ -652,26 +668,20 @@ function buildCompletionReceipt(
|
|
|
652
668
|
frontmatter: Record<string, unknown>
|
|
653
669
|
): { ok: true; receipt: ArtifactReceipt } | { ok: false; message: string } | null {
|
|
654
670
|
if (family === 'code') {
|
|
655
|
-
let content: string;
|
|
656
|
-
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
657
|
-
catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
|
|
658
|
-
const input = parseCodePlanInputReference(content);
|
|
659
|
-
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
660
671
|
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
661
672
|
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
662
673
|
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
|
|
663
|
-
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
664
674
|
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
665
|
-
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !==
|
|
666
|
-
return { ok: false, message: `code input '${
|
|
675
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
|
|
676
|
+
return { ok: false, message: `code input '${startedInput}' is not the latest plan artifact` };
|
|
667
677
|
}
|
|
668
678
|
try {
|
|
669
679
|
const inputSha256 = sha256File(plan.latest.path);
|
|
670
|
-
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${
|
|
680
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${startedInput} changed after code.started` };
|
|
671
681
|
return {
|
|
672
682
|
ok: true,
|
|
673
683
|
receipt: {
|
|
674
|
-
event: 'code.completed', output: artifact.name, input:
|
|
684
|
+
event: 'code.completed', output: artifact.name, input: startedInput,
|
|
675
685
|
inputSha256, completedAt
|
|
676
686
|
}
|
|
677
687
|
};
|
|
@@ -681,18 +691,12 @@ function buildCompletionReceipt(
|
|
|
681
691
|
}
|
|
682
692
|
if (!family.startsWith('review-')) return null;
|
|
683
693
|
const expectedFamily = reviewInputFamily(family);
|
|
684
|
-
let content: string;
|
|
685
|
-
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
686
|
-
catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
|
|
687
|
-
const input = parseReviewedInputReference(content, expectedFamily);
|
|
688
|
-
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
689
694
|
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
690
695
|
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
691
696
|
if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
|
|
692
|
-
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
693
697
|
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
694
|
-
if (current.status !== 'ready' || !current.latest || current.latest.name !==
|
|
695
|
-
return { ok: false, message: `review input '${
|
|
698
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
|
|
699
|
+
return { ok: false, message: `review input '${startedInput}' is not the latest ${expectedFamily} artifact` };
|
|
696
700
|
}
|
|
697
701
|
const event = family === 'review-analysis'
|
|
698
702
|
? 'review-analysis.completed' as const
|
|
@@ -701,16 +705,16 @@ function buildCompletionReceipt(
|
|
|
701
705
|
const stat = fs.lstatSync(current.latest.path);
|
|
702
706
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('review input is not a regular file');
|
|
703
707
|
const inputSha256 = sha256File(current.latest.path);
|
|
704
|
-
if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${
|
|
708
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${startedInput} changed after review.started` };
|
|
705
709
|
return {
|
|
706
710
|
ok: true,
|
|
707
711
|
receipt: {
|
|
708
|
-
event, output: artifact.name, input:
|
|
712
|
+
event, output: artifact.name, input: startedInput,
|
|
709
713
|
inputSha256, completedAt
|
|
710
714
|
}
|
|
711
715
|
};
|
|
712
716
|
} catch (error) {
|
|
713
|
-
return { ok: false, message: `cannot hash review input ${
|
|
717
|
+
return { ok: false, message: `cannot hash review input ${startedInput}: ${error instanceof Error ? error.message : String(error)}` };
|
|
714
718
|
}
|
|
715
719
|
}
|
|
716
720
|
|
|
@@ -724,6 +728,20 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
724
728
|
let content: string;
|
|
725
729
|
try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
726
730
|
catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
731
|
+
const initialParts = eventParts(request.event);
|
|
732
|
+
if (initialParts.phase === 'started' && !request.dryRun) {
|
|
733
|
+
const reconciled = reconcileTaskInvalidation(request.taskRef, { repoRoot: resolved.repoRoot });
|
|
734
|
+
if (reconciled.status === 'failed') {
|
|
735
|
+
return failed(request, {
|
|
736
|
+
code: 'EVENT_TRANSITION_INVALID',
|
|
737
|
+
message: `INVALIDATION_RECONCILE_FAILED: ${reconciled.error?.code ?? 'UNKNOWN'}: ${reconciled.error?.message ?? ''}`
|
|
738
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
739
|
+
}
|
|
740
|
+
if (reconciled.changed) {
|
|
741
|
+
try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
742
|
+
catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
743
|
+
}
|
|
744
|
+
}
|
|
727
745
|
let frontmatter;
|
|
728
746
|
try { frontmatter = parseTypedTaskFrontmatter(content); }
|
|
729
747
|
catch (error) { return failed(request, { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
@@ -732,7 +750,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
732
750
|
const rows = startedBackedRows(pairEntries(section.entries));
|
|
733
751
|
let normalized = request;
|
|
734
752
|
let artifactContext: ArtifactContextResult | null = null;
|
|
735
|
-
const initialParts = eventParts(request.event);
|
|
736
753
|
if (initialParts.phase === 'started') {
|
|
737
754
|
const openIdentity = openStartedIdentity(rows, initialParts.family);
|
|
738
755
|
if (openIdentity && 'conflict' in openIdentity) return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'artifact family has more than one open started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
@@ -969,14 +986,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
969
986
|
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
970
987
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
971
988
|
}
|
|
972
|
-
const reviewStage = eventIdentity.family === 'review-analysis'
|
|
973
|
-
? 'analysis'
|
|
974
|
-
: eventIdentity.family === 'review-plan' ? 'plan' : 'code';
|
|
975
989
|
const actualSha256 = sha256File(completedArtifact.path);
|
|
976
990
|
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
977
|
-
const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
|
|
978
991
|
if (reviewFinalizationIntent.round !== normalized.round
|
|
979
|
-
|| reviewFinalizationIntent.requestId !== expectedRequestId
|
|
980
992
|
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
981
993
|
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
982
994
|
return failed(normalized, {
|
|
@@ -1072,6 +1084,11 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1072
1084
|
} else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
1073
1085
|
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
1074
1086
|
}
|
|
1087
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
|
|
1088
|
+
const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot);
|
|
1089
|
+
if (reviewedCommit) frontmatterSet.last_reviewed_commit = reviewedCommit;
|
|
1090
|
+
else if (normalized.verdict === 'approved') frontmatterSet.last_reviewed_commit = '';
|
|
1091
|
+
}
|
|
1075
1092
|
if (eventIdentity.phase === 'completed') frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
|
|
1076
1093
|
if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
|
|
1077
1094
|
const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
|
|
@@ -1231,7 +1248,7 @@ function applyTaskEvent(request: TaskEventRequest, options: TaskEventOptions = {
|
|
|
1231
1248
|
if (invalid) return failed(request, invalid);
|
|
1232
1249
|
const parts = eventParts(request.event);
|
|
1233
1250
|
if (options.lockAlreadyHeld) return applyTaskEventUnlocked(request, options);
|
|
1234
|
-
if (request.dryRun ||
|
|
1251
|
+
if (request.dryRun || !['started', 'completed'].includes(parts.phase)) {
|
|
1235
1252
|
return applyTaskEventUnlocked(request, options);
|
|
1236
1253
|
}
|
|
1237
1254
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { VerificationStatus } from './verification-types.ts';
|
|
2
|
+
|
|
3
|
+
export type GateClassification = 'hard' | 'soft' | 'info';
|
|
4
|
+
export type PlatformAuditId =
|
|
5
|
+
| 'closed-status-labels' | 'status-label' | 'comment-marker' | 'pr-comment-marker'
|
|
6
|
+
| 'pr-comment-last-commit' | 'pr-comment-content' | 'comment-content' | 'task-comment-content'
|
|
7
|
+
| 'in-labels-computed' | 'pr-type-label' | 'in-labels-match-pr' | 'pr-assignee'
|
|
8
|
+
| 'requirements' | 'issue-type' | 'issue-fields' | 'milestone';
|
|
9
|
+
|
|
10
|
+
export type PlatformAuditPolicy = {
|
|
11
|
+
classification: GateClassification;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
expectedStatusLabel?: string;
|
|
14
|
+
expectedCommentMarkerKey?: 'artifact' | 'summary';
|
|
15
|
+
expectedPrCommentMarkerKey?: 'prSummary';
|
|
16
|
+
requireSpecificMilestone?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type VerificationRecord = {
|
|
20
|
+
type: string;
|
|
21
|
+
status: VerificationStatus;
|
|
22
|
+
message: string;
|
|
23
|
+
fail_type?: string;
|
|
24
|
+
checkId?: string;
|
|
25
|
+
classification?: GateClassification;
|
|
26
|
+
effectiveStatus?: VerificationStatus;
|
|
27
|
+
reason?: string;
|
|
28
|
+
action?: string;
|
|
29
|
+
dependsOn?: string;
|
|
30
|
+
warnings?: string[];
|
|
31
|
+
subchecks?: VerificationRecord[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const OPTIONAL_PLATFORM_AUDITS = new Set<PlatformAuditId>([
|
|
35
|
+
'closed-status-labels', 'status-label', 'comment-marker', 'pr-comment-marker',
|
|
36
|
+
'pr-comment-last-commit', 'pr-comment-content', 'comment-content', 'task-comment-content',
|
|
37
|
+
'in-labels-computed', 'pr-type-label', 'in-labels-match-pr', 'pr-assignee',
|
|
38
|
+
'requirements', 'issue-type', 'issue-fields', 'milestone'
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
42
|
+
'create-task': 'status: waiting-for-triage',
|
|
43
|
+
'analyze-task': 'status: pending-design-work',
|
|
44
|
+
'review-analysis': 'status: pending-design-work',
|
|
45
|
+
'plan-task': 'status: pending-design-work',
|
|
46
|
+
'review-plan': 'status: pending-design-work',
|
|
47
|
+
'code-task': 'status: in-progress',
|
|
48
|
+
'review-code': 'status: in-progress'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const ARTIFACT_COMMENT_SKILLS = new Set([
|
|
52
|
+
'analyze-task', 'plan-task', 'review-analysis', 'review-plan', 'code-task',
|
|
53
|
+
'review-code', 'review-pr', 'run-manual-validation'
|
|
54
|
+
]);
|
|
55
|
+
const PR_AUDIT_SKILLS = new Set(['create-pr']);
|
|
56
|
+
const IN_LABEL_AUDIT_SKILLS = new Set(['code-task', 'commit', 'create-pr']);
|
|
57
|
+
|
|
58
|
+
export function platformAuditPolicy(checkId: PlatformAuditId, context: { skillName?: string; artifactFile?: string | null }): PlatformAuditPolicy {
|
|
59
|
+
const skillName = context.skillName || '';
|
|
60
|
+
const artifactComment = Boolean(context.artifactFile) && ARTIFACT_COMMENT_SKILLS.has(skillName);
|
|
61
|
+
const prAudit = PR_AUDIT_SKILLS.has(skillName);
|
|
62
|
+
const inLabelAudit = IN_LABEL_AUDIT_SKILLS.has(skillName);
|
|
63
|
+
|
|
64
|
+
switch (checkId) {
|
|
65
|
+
case 'status-label':
|
|
66
|
+
return { classification: 'soft', enabled: Boolean(STATUS_LABELS[skillName]), expectedStatusLabel: STATUS_LABELS[skillName] };
|
|
67
|
+
case 'comment-marker':
|
|
68
|
+
return {
|
|
69
|
+
classification: 'soft',
|
|
70
|
+
enabled: artifactComment || skillName === 'complete-task',
|
|
71
|
+
...(artifactComment ? { expectedCommentMarkerKey: 'artifact' } : skillName === 'complete-task' ? { expectedCommentMarkerKey: 'summary' } : {})
|
|
72
|
+
};
|
|
73
|
+
case 'pr-comment-marker':
|
|
74
|
+
return {
|
|
75
|
+
classification: 'soft',
|
|
76
|
+
enabled: skillName === 'commit' || prAudit,
|
|
77
|
+
...(skillName === 'commit' || prAudit ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
78
|
+
};
|
|
79
|
+
case 'pr-comment-last-commit':
|
|
80
|
+
return {
|
|
81
|
+
classification: 'soft',
|
|
82
|
+
enabled: skillName === 'commit',
|
|
83
|
+
...(skillName === 'commit' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
84
|
+
};
|
|
85
|
+
case 'pr-comment-content':
|
|
86
|
+
return {
|
|
87
|
+
classification: 'soft',
|
|
88
|
+
enabled: skillName === 'complete-manual-validation',
|
|
89
|
+
...(skillName === 'complete-manual-validation' ? { expectedPrCommentMarkerKey: 'prSummary' } : {})
|
|
90
|
+
};
|
|
91
|
+
case 'comment-content':
|
|
92
|
+
return { classification: 'soft', enabled: artifactComment, expectedCommentMarkerKey: 'artifact' };
|
|
93
|
+
case 'task-comment-content':
|
|
94
|
+
return { classification: 'soft', enabled: true };
|
|
95
|
+
case 'in-labels-computed':
|
|
96
|
+
return { classification: 'soft', enabled: inLabelAudit };
|
|
97
|
+
case 'pr-type-label':
|
|
98
|
+
case 'in-labels-match-pr':
|
|
99
|
+
case 'pr-assignee':
|
|
100
|
+
return { classification: 'soft', enabled: prAudit };
|
|
101
|
+
case 'requirements':
|
|
102
|
+
return { classification: 'soft', enabled: skillName === 'complete-task' };
|
|
103
|
+
case 'milestone':
|
|
104
|
+
return { classification: 'soft', enabled: true, requireSpecificMilestone: skillName === 'code-task' };
|
|
105
|
+
case 'closed-status-labels':
|
|
106
|
+
case 'issue-type':
|
|
107
|
+
case 'issue-fields':
|
|
108
|
+
return { classification: 'soft', enabled: true };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function classificationForCheck(checkId: string): GateClassification {
|
|
113
|
+
return checkId.startsWith('platform.') && OPTIONAL_PLATFORM_AUDITS.has(checkId.slice('platform.'.length) as PlatformAuditId)
|
|
114
|
+
? 'soft'
|
|
115
|
+
: 'hard';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function effectiveStatus(status: VerificationStatus, classification: GateClassification): VerificationStatus {
|
|
119
|
+
return classification === 'hard' ? status : 'pass';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizeVerificationRecord(record: VerificationRecord): VerificationRecord {
|
|
123
|
+
const checkId = record.checkId ?? record.type;
|
|
124
|
+
const classification = record.classification ?? classificationForCheck(checkId);
|
|
125
|
+
return {
|
|
126
|
+
...record,
|
|
127
|
+
checkId,
|
|
128
|
+
classification,
|
|
129
|
+
effectiveStatus: record.effectiveStatus ?? effectiveStatus(record.status, classification),
|
|
130
|
+
reason: record.reason ?? record.fail_type ?? (classification === 'info' ? 'NOT_APPLICABLE' : record.status === 'pass' ? 'OK' : 'CHECK_FAILED'),
|
|
131
|
+
action: record.action ?? (classification === 'info' || record.status === 'pass' ? 'No action required' : 'Review the check evidence and retry after correction')
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -5,7 +5,6 @@ import { scanVisibleMarkdown } from './markdown.ts';
|
|
|
5
5
|
import { parseArtifactName } from './artifact-name.ts';
|
|
6
6
|
import {
|
|
7
7
|
hasOpenArtifactRound,
|
|
8
|
-
parseCodePlanInputReference,
|
|
9
8
|
validateArtifactPublication,
|
|
10
9
|
validateCompletedArtifact
|
|
11
10
|
} from './artifact-lifecycle.ts';
|
|
@@ -206,12 +205,15 @@ function validateLocalArtifact(
|
|
|
206
205
|
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
207
206
|
|
|
208
207
|
if (options.taskContent !== undefined) {
|
|
209
|
-
const
|
|
210
|
-
|
|
208
|
+
const identity = options.artifact ? parseArtifactName(options.artifact) : null;
|
|
209
|
+
const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
|
|
210
|
+
? expectedQualificationRelations(options.taskContent, options.family)
|
|
211
|
+
: undefined;
|
|
212
|
+
if (expected && !expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
211
213
|
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
212
214
|
family: options.family,
|
|
213
215
|
artifact: options.artifact,
|
|
214
|
-
expectedUpstreamRelations: expected
|
|
216
|
+
expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
|
|
215
217
|
});
|
|
216
218
|
if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
217
219
|
}
|
|
@@ -57,8 +57,7 @@ import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead,
|
|
|
57
57
|
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.ts';
|
|
58
58
|
import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.ts';
|
|
59
59
|
import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
60
|
-
import {
|
|
61
|
-
import type { InvalidationDocument } from './invalidation.ts';
|
|
60
|
+
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
62
61
|
import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
|
|
63
62
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
64
63
|
|
|
@@ -678,13 +677,11 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
678
677
|
}
|
|
679
678
|
let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
|
|
680
679
|
let currentLedger: LedgerDocument | null = null;
|
|
681
|
-
let currentInvalidation: InvalidationDocument = { operations: [], targets: [] };
|
|
682
680
|
if (resolved.state === 'active') {
|
|
683
681
|
const contract = validateCurrentTaskContract(content);
|
|
684
682
|
if (!contract.ok) return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
|
|
685
683
|
metadata = contract.metadata;
|
|
686
684
|
currentLedger = contract.ledger;
|
|
687
|
-
currentInvalidation = contract.invalidation;
|
|
688
685
|
} else {
|
|
689
686
|
try {
|
|
690
687
|
metadata = parseTypedTaskFrontmatter(content);
|
|
@@ -692,9 +689,6 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
692
689
|
return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
693
690
|
}
|
|
694
691
|
}
|
|
695
|
-
if (invalidationBlocks(currentInvalidation)) {
|
|
696
|
-
return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
|
|
697
|
-
}
|
|
698
692
|
let run: OrchestrationRun | null;
|
|
699
693
|
try {
|
|
700
694
|
run = readRun(resolved.taskDir, options);
|
|
@@ -711,6 +705,9 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
711
705
|
const routed = routeFromFacts(facts.facts);
|
|
712
706
|
if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
|
|
713
707
|
if ('completion' in routed) {
|
|
708
|
+
if (facts.facts.executionBusy && !run?.pendingDelegation) {
|
|
709
|
+
return failed('ORCHESTRATION_EXECUTION_BUSY', 'clean completion requires no open lifecycle execution', resolved.taskId);
|
|
710
|
+
}
|
|
714
711
|
const reviewRound = maxArtifactRound(fs.readdirSync(resolved.taskDir), 'review-code');
|
|
715
712
|
const review = parseReviewSummary(fs.readFileSync(
|
|
716
713
|
path.join(resolved.taskDir, artifactName('review-code', reviewRound)),
|
|
@@ -847,6 +844,14 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
847
844
|
return failed('ORCHESTRATION_DELEGATION_BUSY', 'the repository already has a pending lifecycle delegation', resolved.taskId);
|
|
848
845
|
}
|
|
849
846
|
if (run.stepCount >= run.maxSteps) return pauseOrchestration(taskRef, 'ORCHESTRATION_MAX_STEPS', 'maximum orchestration steps reached', true, options);
|
|
847
|
+
const invalidation = reconcileTaskInvalidation(taskRef, { repoRoot: resolved.repoRoot });
|
|
848
|
+
if (invalidation.status === 'failed') {
|
|
849
|
+
return failed(
|
|
850
|
+
'ORCHESTRATION_INVALIDATION_RECONCILE_FAILED',
|
|
851
|
+
`${invalidation.error?.code ?? 'UNKNOWN'}: ${invalidation.error?.message ?? ''}`,
|
|
852
|
+
resolved.taskId
|
|
853
|
+
);
|
|
854
|
+
}
|
|
850
855
|
const routed = routeOrchestration(taskRef, options);
|
|
851
856
|
if (!routed.next) return routed;
|
|
852
857
|
const next = routed.next;
|
|
@@ -176,19 +176,25 @@ function equalCounts(left: ReviewFindingCounts, right: ReviewFindingCounts): boo
|
|
|
176
176
|
|
|
177
177
|
function finalizeReviewSummaryContent(
|
|
178
178
|
content: string,
|
|
179
|
-
counts: ReviewFindingCounts
|
|
179
|
+
counts: ReviewFindingCounts,
|
|
180
|
+
options: { refreshNumericCounts?: boolean } = {}
|
|
180
181
|
): ReviewSummaryFinalizeResult {
|
|
181
182
|
const parsed = parseReviewSummary(content);
|
|
182
183
|
if (!parsed.ok) return parsed;
|
|
183
184
|
if (parsed.summary.countState === 'numeric') {
|
|
184
185
|
if (equalCounts(parsed.summary.counts!, counts)) return { ok: true, changed: false, content };
|
|
185
|
-
return {
|
|
186
|
+
if (!options.refreshNumericCounts) return {
|
|
186
187
|
ok: false,
|
|
187
188
|
code: 'REVIEW_SUMMARY_COUNT_MISMATCH',
|
|
188
189
|
message: 'review summary finding counts do not match the ledger snapshot'
|
|
189
190
|
};
|
|
190
191
|
}
|
|
191
192
|
let line = content.slice(parsed.findingsStart, parsed.findingsEnd);
|
|
193
|
+
if (parsed.summary.countState === 'numeric') {
|
|
194
|
+
const values = [counts.blocker, counts.major, counts.minor];
|
|
195
|
+
let ordinal = 0;
|
|
196
|
+
line = line.replace(/\d+(?=\s*(?:阻塞项|主要|次要|blockers?\b|majors?\b|minors?\b))/gi, () => String(values[ordinal++]));
|
|
197
|
+
}
|
|
192
198
|
line = line
|
|
193
199
|
.replace(PLACEHOLDERS[0], String(counts.blocker))
|
|
194
200
|
.replace(PLACEHOLDERS[1], String(counts.major))
|