@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.
Files changed (80) hide show
  1. package/dist/lib/internal/manual-validation.js +6 -2
  2. package/dist/lib/platform/issues.js +3 -4
  3. package/dist/lib/platform/verification-sync.js +142 -154
  4. package/dist/lib/sandbox/control/workflow-executor.js +7 -1
  5. package/dist/lib/task/artifact-lifecycle.js +15 -52
  6. package/dist/lib/task/artifact-operations.js +27 -1
  7. package/dist/lib/task/capabilities.js +0 -2
  8. package/dist/lib/task/events.js +60 -43
  9. package/dist/lib/task/gate-policy.js +94 -0
  10. package/dist/lib/task/local-artifact-finalization.js +7 -4
  11. package/dist/lib/task/orchestration.js +8 -6
  12. package/dist/lib/task/review-artifacts.js +12 -6
  13. package/dist/lib/task/review-finalization.js +3 -9
  14. package/dist/lib/task/verification-engine.js +28 -17
  15. package/dist/lib/task/verification.js +25 -9
  16. package/dist/lifecycle-build-manifest.json +3 -3
  17. package/lib/internal/manual-validation.ts +6 -2
  18. package/lib/platform/issues.ts +3 -4
  19. package/lib/platform/verification-sync.ts +159 -177
  20. package/lib/sandbox/control/workflow-executor.ts +11 -1
  21. package/lib/task/artifact-lifecycle.ts +12 -49
  22. package/lib/task/artifact-operations.ts +27 -1
  23. package/lib/task/capabilities.ts +0 -1
  24. package/lib/task/events.ts +49 -32
  25. package/lib/task/gate-policy.ts +133 -0
  26. package/lib/task/local-artifact-finalization.ts +6 -4
  27. package/lib/task/orchestration.ts +12 -7
  28. package/lib/task/review-artifacts.ts +8 -2
  29. package/lib/task/review-finalization.ts +3 -9
  30. package/lib/task/verification-engine.ts +24 -14
  31. package/lib/task/verification-types.ts +7 -0
  32. package/lib/task/verification.ts +25 -9
  33. package/package.json +1 -1
  34. package/templates/.agents/rules/issue-sync.en.md +2 -0
  35. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
  36. package/templates/.agents/rules/local-artifact-repair.en.md +11 -3
  37. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +11 -3
  38. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  39. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  40. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  41. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  42. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  43. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  44. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  45. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  46. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  47. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  48. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  49. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  50. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  51. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  52. package/templates/.agents/skills/commit/config/verify.json +1 -5
  53. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  54. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  55. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  56. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  57. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  58. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  59. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  60. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  61. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  62. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  63. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  64. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
  65. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  66. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  67. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  68. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  69. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  70. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  71. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  72. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  73. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  74. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  75. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  76. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  77. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  78. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  79. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  80. 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 { prepareReviewSummaryCandidate, commitReviewSummaryProvenance } from '../../task/review-finalization.ts';
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, parsed.name)) {
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(`${parsed.name} content digest does not match receipt`);
239
+ throw new Error(`${input.name} content digest does not match receipt`);
254
240
  }
255
- return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
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, `${parsed.name}: ${String(error)}`));
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
- parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName,
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 content = renderArtifactSkeleton({
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' });
@@ -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 {
@@ -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 !== input.name) {
666
- return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
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 ${input.name} changed after code.started` };
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: input.name,
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 !== input.name) {
695
- return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
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 ${input.name} changed after review.started` };
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: input.name,
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 ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
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 || parts.phase !== 'completed' || parts.family === 'manual-validation') {
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 expected = expectedQualificationRelations(options.taskContent, options.family);
210
- if (!expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
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.ok ? expected.relations : undefined
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 { invalidationBlocks } from './invalidation.ts';
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))