@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
@@ -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, parsed.name)) {
176
- throw new Error(`${parsed.name} is invalidated`);
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(`${parsed.name} content digest does not match receipt`);
173
+ throw new Error(`${input.name} content digest does not match receipt`);
188
174
  }
189
- return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
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, `${parsed.name}: ${String(error)}`));
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, parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
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 content = renderArtifactSkeleton({
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 {
@@ -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, parseReviewedInputReference, parseCodePlanInputReference, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
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 !== input.name) {
616
- return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
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 ${input.name} changed after code.started` };
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: input.name,
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 !== input.name) {
655
- return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
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 ${input.name} changed after review.started` };
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: input.name,
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 ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
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 || parts.phase !== 'completed' || parts.family === 'manual-validation') {
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, parseCodePlanInputReference, validateArtifactPublication, validateCompletedArtifact } from './artifact-lifecycle.js';
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 expected = expectedQualificationRelations(options.taskContent, options.family);
101
- if (!expected.ok)
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.ok ? expected.relations : undefined
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 { invalidationBlocks } from './invalidation.js';
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
- return {
129
- ok: false,
130
- code: 'REVIEW_SUMMARY_COUNT_MISMATCH',
131
- message: 'review summary finding counts do not match the ledger snapshot'
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))