@fitlab-ai/agent-infra 0.11.6 → 0.11.8

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 (192) hide show
  1. package/dist/bin/internal-cli.js +8 -2
  2. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  3. package/dist/lib/agent-clients/config.js +11 -3
  4. package/dist/lib/defaults.json +6 -3
  5. package/dist/lib/git/primary-worktree.js +58 -0
  6. package/dist/lib/internal/cli-route-inventory.js +3 -0
  7. package/dist/lib/internal/platform-pr-review.js +20 -3
  8. package/dist/lib/internal/task-operation-registry.js +3 -1
  9. package/dist/lib/internal/task-platform-recovery.js +56 -0
  10. package/dist/lib/merge.js +25 -5
  11. package/dist/lib/platform/completion-backfill.js +2 -1
  12. package/dist/lib/platform/in-label-sync.js +12 -1
  13. package/dist/lib/platform/issue-comments.js +58 -4
  14. package/dist/lib/platform/issues.js +105 -5
  15. package/dist/lib/platform/operation-coordinator.js +74 -0
  16. package/dist/lib/platform/pr-review.js +107 -1
  17. package/dist/lib/platform/pr-summary.js +79 -57
  18. package/dist/lib/platform/pull-requests.js +125 -7
  19. package/dist/lib/sandbox/commands/create.js +6 -3
  20. package/dist/lib/sandbox/commands/ls.js +4 -3
  21. package/dist/lib/sandbox/commands/rm.js +1 -1
  22. package/dist/lib/sandbox/config.js +22 -4
  23. package/dist/lib/sandbox/control/server.js +6 -2
  24. package/dist/lib/sandbox/git-metadata.js +50 -0
  25. package/dist/lib/sandbox/index.js +1 -1
  26. package/dist/lib/sandbox/mounts.js +12 -0
  27. package/dist/lib/sandbox/recovery.js +11 -0
  28. package/dist/lib/sandbox/removal.js +58 -20
  29. package/dist/lib/sandbox/tools.js +29 -7
  30. package/dist/lib/server/config.js +7 -27
  31. package/dist/lib/server/daemon.js +4 -3
  32. package/dist/lib/server/process-control.js +4 -2
  33. package/dist/lib/server/runner.js +4 -3
  34. package/dist/lib/task/activity-intent.js +4 -3
  35. package/dist/lib/task/artifact-lifecycle.js +42 -10
  36. package/dist/lib/task/artifact-receipts.js +62 -22
  37. package/dist/lib/task/capabilities.js +22 -4
  38. package/dist/lib/task/completion-facts.js +56 -0
  39. package/dist/lib/task/create-service.js +11 -5
  40. package/dist/lib/task/create.js +60 -79
  41. package/dist/lib/task/events.js +268 -195
  42. package/dist/lib/task/finalization-artifacts.js +4 -1
  43. package/dist/lib/task/finalization-handoff.js +2 -1
  44. package/dist/lib/task/finalization.js +157 -24
  45. package/dist/lib/task/invalidation-command.js +4 -2
  46. package/dist/lib/task/invalidation.js +38 -9
  47. package/dist/lib/task/lifecycle.js +38 -2
  48. package/dist/lib/task/local-artifact-finalization.js +17 -3
  49. package/dist/lib/task/platform-operation-journal.js +152 -0
  50. package/dist/lib/task/platform-operation-recovery.js +459 -0
  51. package/dist/lib/task/qualification-audit.js +23 -105
  52. package/dist/lib/task/review-finalization.js +7 -0
  53. package/dist/lib/task/review-identity.js +69 -0
  54. package/dist/lib/task/sections.js +6 -5
  55. package/dist/lib/task/task-execution-lock.js +6 -3
  56. package/dist/lib/task/verification-engine.js +14 -63
  57. package/dist/lib/task/verification.js +35 -38
  58. package/dist/lib/task/workflow-warning-intents.js +7 -1
  59. package/dist/lifecycle-build-manifest.json +3 -3
  60. package/lib/agent-clients/adapters/codex.ts +1 -1
  61. package/lib/agent-clients/config.ts +9 -3
  62. package/lib/defaults.json +6 -3
  63. package/lib/git/primary-worktree.ts +62 -0
  64. package/lib/internal/cli-route-inventory.ts +2 -0
  65. package/lib/internal/platform-pr-review.ts +14 -3
  66. package/lib/internal/task-operation-registry.ts +3 -1
  67. package/lib/internal/task-platform-recovery.ts +53 -0
  68. package/lib/merge.ts +26 -6
  69. package/lib/platform/completion-backfill.ts +2 -1
  70. package/lib/platform/in-label-sync.ts +12 -0
  71. package/lib/platform/issue-comments.ts +59 -3
  72. package/lib/platform/issues.ts +103 -5
  73. package/lib/platform/operation-coordinator.ts +96 -0
  74. package/lib/platform/pr-review.ts +113 -1
  75. package/lib/platform/pr-summary.ts +22 -1
  76. package/lib/platform/pull-requests.ts +132 -8
  77. package/lib/sandbox/commands/create.ts +7 -8
  78. package/lib/sandbox/commands/ls.ts +4 -3
  79. package/lib/sandbox/commands/rm.ts +1 -1
  80. package/lib/sandbox/config.ts +23 -5
  81. package/lib/sandbox/control/server.ts +5 -2
  82. package/lib/sandbox/git-metadata.ts +66 -0
  83. package/lib/sandbox/index.ts +1 -1
  84. package/lib/sandbox/mounts.ts +14 -0
  85. package/lib/sandbox/recovery.ts +13 -0
  86. package/lib/sandbox/removal.ts +57 -19
  87. package/lib/sandbox/tool-types.ts +1 -1
  88. package/lib/sandbox/tools.ts +36 -7
  89. package/lib/server/config.ts +7 -29
  90. package/lib/server/daemon.ts +5 -3
  91. package/lib/server/process-control.ts +4 -2
  92. package/lib/server/runner.ts +7 -4
  93. package/lib/server/server.schema.json +0 -4
  94. package/lib/task/activity-intent.ts +4 -3
  95. package/lib/task/artifact-lifecycle.ts +35 -11
  96. package/lib/task/artifact-receipts.ts +55 -15
  97. package/lib/task/capabilities.ts +17 -3
  98. package/lib/task/completion-facts.ts +66 -0
  99. package/lib/task/create-service.ts +12 -6
  100. package/lib/task/create.ts +54 -69
  101. package/lib/task/events.ts +248 -182
  102. package/lib/task/finalization-artifacts.ts +4 -1
  103. package/lib/task/finalization-handoff.ts +2 -1
  104. package/lib/task/finalization.ts +154 -21
  105. package/lib/task/invalidation-command.ts +4 -2
  106. package/lib/task/invalidation.ts +35 -10
  107. package/lib/task/lifecycle.ts +35 -3
  108. package/lib/task/local-artifact-finalization.ts +13 -3
  109. package/lib/task/platform-operation-journal.ts +230 -0
  110. package/lib/task/platform-operation-recovery.ts +435 -0
  111. package/lib/task/qualification-audit.ts +21 -114
  112. package/lib/task/review-finalization.ts +7 -0
  113. package/lib/task/review-identity.ts +85 -0
  114. package/lib/task/sections.ts +6 -6
  115. package/lib/task/task-execution-lock.ts +7 -3
  116. package/lib/task/verification-engine.ts +13 -86
  117. package/lib/task/verification.ts +34 -38
  118. package/lib/task/workflow-warning-intents.ts +9 -1
  119. package/package.json +1 -1
  120. package/templates/.agents/README.en.md +19 -17
  121. package/templates/.agents/README.zh-CN.md +18 -16
  122. package/templates/.agents/rules/README.en.md +2 -1
  123. package/templates/.agents/rules/README.zh-CN.md +2 -1
  124. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  125. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  126. package/templates/.agents/rules/task-management.en.md +12 -0
  127. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  128. package/templates/.agents/rules/validation-output.en.md +19 -0
  129. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  130. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  131. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  132. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  133. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  134. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
  135. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
  136. package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
  137. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
  138. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  139. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  140. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  142. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  143. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  144. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  145. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  146. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  147. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  148. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  149. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  150. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  151. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  152. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  153. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  155. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  157. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  158. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  159. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  160. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  161. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  162. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  163. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  164. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  165. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  166. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  167. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  168. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  169. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  170. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  171. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  172. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  173. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  174. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  175. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  176. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  177. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  178. package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
  179. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
  180. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  181. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  182. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  183. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  184. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  191. package/templates/.agents/templates/task.en.md +2 -2
  192. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -1,7 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
3
  import { parseArtifactName } from './artifact-name.ts';
4
- import { parseTypedTaskFrontmatter } from './frontmatter.ts';
5
4
  import { findSectionRange, parseTable } from './sections.ts';
6
5
  import { scanVisibleMarkdown } from './markdown.ts';
7
6
 
@@ -9,13 +8,12 @@ const TASK_CONSTRAINT_HEADINGS = ['约束', 'Constraints'] as const;
9
8
  const TASK_CANDIDATE_HEADINGS = ['候选与否决方案', 'Candidate and Rejected Options', 'Candidates and Rejected Options', 'Candidates and Rejected Alternatives'] as const;
10
9
  const AUDIT_HEADINGS = ['资格审计', '资格审计复核', 'Qualification Audit', 'Qualification Audit Review'] as const;
11
10
  const CONFIRMATION_HEADINGS = ['资格确认记录', 'Qualification Confirmations'] as const;
12
- const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '上游关系', '依赖快照'] as const;
11
+ const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '资格快照'] as const;
13
12
  const AUDIT_SUBSECTION_HEADINGS: Record<(typeof AUDIT_SUBSECTIONS)[number], readonly string[]> = {
14
13
  '约束依赖': ['约束依赖', 'Constraint Dependencies'],
15
14
  '候选资格': ['候选资格', 'Candidate Qualification'],
16
15
  '分类结果': ['分类结果', 'Classification Results'],
17
- '上游关系': ['上游关系', 'Upstream Relations'],
18
- '依赖快照': ['依赖快照', 'Dependency Snapshot']
16
+ '资格快照': ['资格快照', 'Qualification Snapshot']
19
17
  };
20
18
 
21
19
  const CONSTRAINT_COLUMNS = ['constraint_id', 'statement', 'status', 'authority', 'source', 'evidence', 'derived_from', 'approval_evidence'] as const;
@@ -23,12 +21,10 @@ const CANDIDATE_COLUMNS = ['candidate_id', 'statement', 'status', 'constraint_id
23
21
  const DEPENDENCY_COLUMNS = ['constraint_id', 'constraint_digest', 'role', 'evidence'] as const;
24
22
  const QUALIFICATION_COLUMNS = ['candidate_id', 'status', 'impact', 'constraint_ids', 'evidence'] as const;
25
23
  const CLASSIFICATION_COLUMNS = ['decision_id', 'classification', 'evidence'] as const;
26
- const RELATION_COLUMNS = ['upstream_family', 'upstream_artifact', 'upstream_round', 'upstream_sha256', 'relation'] as const;
27
- const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest', 'upstream_artifact_digest'] as const;
24
+ const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest'] as const;
28
25
  const CONFIRMATION_COLUMNS = ['qcr_id', 'constraint_id', 'actor', 'entrypoint', 'request_id', 'approved_digest', 'confirmed_at', 'rationale'] as const;
29
26
 
30
27
  const ARTIFACT_FAMILIES = ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'] as const;
31
- const RELATIONS = ['required-input', 'reviewed-input', 'review-context', 'approval-context'] as const;
32
28
  const CONSTRAINT_STATUSES = ['confirmed', 'derived', 'assumption', 'open', 'conflicted', 'superseded'] as const;
33
29
  const CANDIDATE_STATUSES = ['qualified', 'rejected', 'pending', 'needs-human-decision', 'confirmed', 'excluded'] as const;
34
30
  const CLASSIFICATIONS = ['deterministic', 'qualified', 'rejected', 'needs-human-decision', 'not-applicable'] as const;
@@ -38,7 +34,6 @@ type ConstraintStatus = (typeof CONSTRAINT_STATUSES)[number];
38
34
  type CandidateStatus = (typeof CANDIDATE_STATUSES)[number];
39
35
  type QualificationClassification = (typeof CLASSIFICATIONS)[number];
40
36
  type ArtifactFamily = (typeof ARTIFACT_FAMILIES)[number];
41
- type QualificationRelation = (typeof RELATIONS)[number];
42
37
  type QualificationRole = (typeof ROLES)[number];
43
38
 
44
39
  type Constraint = {
@@ -86,24 +81,15 @@ type QualificationClassificationRow = {
86
81
  classification: QualificationClassification;
87
82
  evidence: string;
88
83
  };
89
- type UpstreamRelation = {
90
- upstreamFamily: ArtifactFamily;
91
- upstreamArtifact: string;
92
- upstreamRound: number;
93
- upstreamSha256: string;
94
- relation: QualificationRelation;
95
- };
96
84
  type DependencySnapshot = {
97
85
  taskInputDigest: string;
98
86
  nonConstraintInputDigest: string;
99
- upstreamArtifactDigest: string;
100
87
  };
101
88
  type QualificationAudit = {
102
89
  present: boolean;
103
90
  constraintDependencies: readonly ConstraintDependency[];
104
91
  candidateQualifications: readonly CandidateQualification[];
105
92
  classifications: readonly QualificationClassificationRow[];
106
- upstreamRelations: readonly UpstreamRelation[];
107
93
  snapshot: DependencySnapshot | null;
108
94
  };
109
95
  type QualificationConfirmation = {
@@ -304,80 +290,19 @@ function parseAuditTable(content: string, heading: (typeof AUDIT_SUBSECTIONS)[nu
304
290
  return table?.rows.map((row) => ({ ...row.values })) ?? null;
305
291
  }
306
292
 
307
- function parseAuditArtifactName(value: string): { family: ArtifactFamily; round: number } | null {
308
- const identity = parseArtifactName(value);
309
- return identity && ARTIFACT_FAMILIES.includes(identity.family as ArtifactFamily)
310
- ? { family: identity.family as ArtifactFamily, round: identity.round } : null;
311
- }
312
-
313
- function upstreamArtifactDigest(rows: readonly UpstreamRelation[]): string {
314
- return digest([...rows].map((row) => ({ ...row })).sort((a, b) =>
315
- `${a.upstreamFamily}/${a.upstreamArtifact}/${a.relation}`.localeCompare(`${b.upstreamFamily}/${b.upstreamArtifact}/${b.relation}`)));
316
- }
317
-
318
- function validateUpstreamRelation(row: UpstreamRelation): void {
319
- const identity = parseAuditArtifactName(row.upstreamArtifact);
320
- if (!ARTIFACT_FAMILIES.includes(row.upstreamFamily) || !identity || identity.family !== row.upstreamFamily
321
- || identity.round !== row.upstreamRound || !Number.isSafeInteger(row.upstreamRound)
322
- || !/^[a-f0-9]{64}$/i.test(row.upstreamSha256) || !RELATIONS.includes(row.relation)) {
323
- throw new Error(`invalid qualification upstream relation '${row.upstreamArtifact}'`);
324
- }
325
- }
326
-
327
- function expectedQualificationRelations(
328
- taskContent: string,
329
- family: ArtifactFamily
330
- ): { ok: true; relations: readonly UpstreamRelation[] | undefined } | { ok: false; code: string; message: string } {
331
- let frontmatter;
332
- try { frontmatter = parseTypedTaskFrontmatter(taskContent); }
333
- catch (error) { return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) }; }
334
- const encoded = frontmatter.qualification_input_relations;
335
- if (typeof encoded === 'string' && encoded) {
336
- try {
337
- const parsed = JSON.parse(encoded) as unknown;
338
- if (!Array.isArray(parsed)) throw new Error('qualification_input_relations must be an array');
339
- const relations = parsed.map((value) => {
340
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('qualification input relation must be an object');
341
- const row = value as Record<string, unknown>;
342
- const relation: UpstreamRelation = {
343
- upstreamFamily: row.upstreamFamily as ArtifactFamily,
344
- upstreamArtifact: String(row.upstreamArtifact ?? ''),
345
- upstreamRound: Number(row.upstreamRound),
346
- upstreamSha256: String(row.upstreamSha256 ?? ''),
347
- relation: row.relation as QualificationRelation
348
- };
349
- validateUpstreamRelation(relation);
350
- return relation;
351
- });
352
- return { ok: true, relations };
353
- } catch (error) {
354
- return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
355
- }
356
- }
357
- const inputName = family === 'code' ? frontmatter.code_input_artifact : family.startsWith('review-') ? frontmatter.review_input_artifact : '';
358
- const inputSha256 = family === 'code' ? frontmatter.code_input_sha256 : family.startsWith('review-') ? frontmatter.review_input_sha256 : '';
359
- if (typeof inputName !== 'string' || !inputName || typeof inputSha256 !== 'string' || !inputSha256) return { ok: true, relations: undefined };
360
- const identity = parseAuditArtifactName(inputName);
361
- if (!identity) return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: `started input '${inputName}' is not canonical` };
362
- const relation: UpstreamRelation = {
363
- upstreamFamily: identity.family, upstreamArtifact: inputName, upstreamRound: identity.round,
364
- upstreamSha256: inputSha256, relation: family === 'code' ? 'required-input' : 'reviewed-input'
365
- };
366
- try { validateUpstreamRelation(relation); }
367
- catch (error) { return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) }; }
368
- return { ok: true, relations: [relation] };
369
- }
370
-
371
293
  function parseQualificationAudit(content: string): { ok: true; audit: QualificationAudit } | { ok: false; code: string; message: string } {
372
- if (sectionBody(content, AUDIT_HEADINGS) === null) return { ok: true, audit: { present: false, constraintDependencies: [], candidateQualifications: [], classifications: [], upstreamRelations: [], snapshot: null } };
294
+ const auditBody = sectionBody(content, AUDIT_HEADINGS);
295
+ if (auditBody === null) return { ok: true, audit: { present: false, constraintDependencies: [], candidateQualifications: [], classifications: [], snapshot: null } };
373
296
  try {
297
+ if (sectionBody(auditBody, ['上游关系', 'Upstream Relations'], 3) !== null) {
298
+ throw new Error('qualification audit uses the obsolete lifecycle-relation format; regenerate it using the current format');
299
+ }
374
300
  const dependencies = parseAuditTable(content, '约束依赖', DEPENDENCY_COLUMNS);
375
301
  const qualifications = parseAuditTable(content, '候选资格', QUALIFICATION_COLUMNS);
376
302
  const classifications = parseAuditTable(content, '分类结果', CLASSIFICATION_COLUMNS);
377
- const relations = parseAuditTable(content, '上游关系', RELATION_COLUMNS);
378
- const snapshots = parseAuditTable(content, '依赖快照', SNAPSHOT_COLUMNS);
379
- if (!dependencies || !qualifications || !classifications || !relations || !snapshots || snapshots.length !== 1) {
380
- throw new Error('qualification audit requires all five canonical tables and exactly one dependency snapshot');
303
+ const snapshots = parseAuditTable(content, '资格快照', SNAPSHOT_COLUMNS);
304
+ if (!dependencies || !qualifications || !classifications || !snapshots || snapshots.length !== 1) {
305
+ throw new Error('qualification audit requires three decision tables and exactly one qualification snapshot');
381
306
  }
382
307
  const constraintDependencies = dependencies.map((row) => {
383
308
  if (!/^C-[1-9]\d*$/.test(row.constraint_id ?? '') || !/^[a-f0-9]{64}$/i.test(row.constraint_digest ?? '') || !ROLES.includes(row.role as QualificationRole) || !row.evidence?.trim()) throw new Error(`invalid qualification constraint dependency '${row.constraint_id ?? ''}'`);
@@ -392,19 +317,14 @@ function parseQualificationAudit(content: string): { ok: true; audit: Qualificat
392
317
  if (!row.decision_id?.trim() || !CLASSIFICATIONS.includes(row.classification as QualificationClassification) || !row.evidence?.trim()) throw new Error(`invalid qualification classification '${row.decision_id ?? ''}'`);
393
318
  return { decisionId: row.decision_id, classification: row.classification as QualificationClassification, evidence: row.evidence };
394
319
  });
395
- const upstreamRelations = relations.map((row) => {
396
- const relation = { upstreamFamily: row.upstream_family as ArtifactFamily, upstreamArtifact: row.upstream_artifact ?? '', upstreamRound: Number(row.upstream_round), upstreamSha256: row.upstream_sha256 ?? '', relation: row.relation as QualificationRelation };
397
- validateUpstreamRelation(relation);
398
- return relation;
399
- });
400
320
  const snapshotRow = snapshots[0]!;
401
- if (!/^[a-f0-9]{64}$/i.test(snapshotRow.task_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.non_constraint_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.upstream_artifact_digest ?? '')) throw new Error('qualification dependency snapshot has invalid digest');
321
+ if (!/^[a-f0-9]{64}$/i.test(snapshotRow.task_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.non_constraint_input_digest ?? '')) throw new Error('qualification snapshot has invalid digest');
402
322
  return {
403
323
  ok: true,
404
324
  audit: {
405
325
  present: true, constraintDependencies, candidateQualifications,
406
- classifications: classificationRows, upstreamRelations,
407
- snapshot: { taskInputDigest: snapshotRow.task_input_digest!, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest!, upstreamArtifactDigest: snapshotRow.upstream_artifact_digest! }
326
+ classifications: classificationRows,
327
+ snapshot: { taskInputDigest: snapshotRow.task_input_digest!, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest! }
408
328
  }
409
329
  };
410
330
  } catch (error) {
@@ -443,14 +363,13 @@ function validateQualificationAudit(
443
363
  family?: ArtifactFamily;
444
364
  artifact?: string;
445
365
  require?: boolean;
446
- expectedUpstreamRelations?: readonly UpstreamRelation[];
447
366
  } = {}
448
367
  ): QualificationValidationResult {
449
368
  const task = parseTaskQualification(taskContent);
450
369
  if (!task.ok) return task;
451
370
  const audit = parseQualificationAudit(artifactContent);
452
371
  if (!audit.ok) return audit;
453
- if (!task.qualification.present && !options.require) return { ok: true, qualification: task.qualification, audit: audit.audit };
372
+ if (!audit.audit.present && !options.require) return { ok: true, qualification: task.qualification, audit: audit.audit };
454
373
  if (!task.qualification.present) return { ok: false, code: 'QUALIFICATION_TASK_CONTRACT_MISSING', message: 'task qualification contract is missing' };
455
374
  if (!audit.audit.present) return { ok: false, code: 'QUALIFICATION_AUDIT_MISSING', message: 'artifact qualification audit is missing' };
456
375
  const constraintMap = new Map(task.qualification.constraints.map((row) => [row.constraintId, row]));
@@ -490,13 +409,8 @@ function validateQualificationAudit(
490
409
  const snapshot = audit.audit.snapshot!;
491
410
  if (snapshot.taskInputDigest !== task.qualification.taskInputDigest) return { ok: false, code: 'QUALIFICATION_TASK_DIGEST_MISMATCH', message: 'qualification audit task input digest is stale' };
492
411
  if (snapshot.nonConstraintInputDigest !== task.qualification.nonConstraintInputDigest) return { ok: false, code: 'QUALIFICATION_NON_CONSTRAINT_DIGEST_MISMATCH', message: 'qualification audit non-constraint input digest is stale' };
493
- if (snapshot.upstreamArtifactDigest !== upstreamArtifactDigest(audit.audit.upstreamRelations)) return { ok: false, code: 'QUALIFICATION_UPSTREAM_DIGEST_MISMATCH', message: 'qualification audit upstream digest does not match its relation rows' };
494
- if (options.expectedUpstreamRelations && (
495
- options.expectedUpstreamRelations.length !== audit.audit.upstreamRelations.length
496
- || upstreamArtifactDigest(options.expectedUpstreamRelations) !== upstreamArtifactDigest(audit.audit.upstreamRelations)
497
- )) return { ok: false, code: 'QUALIFICATION_UPSTREAM_RELATION_MISMATCH', message: 'qualification audit upstream relations do not match the started artifact inputs' };
498
412
  if (options.family && options.artifact) {
499
- const identity = parseAuditArtifactName(options.artifact);
413
+ const identity = parseArtifactName(options.artifact);
500
414
  if (!identity || identity.family !== options.family) return { ok: false, code: 'QUALIFICATION_ARTIFACT_IDENTITY_INVALID', message: `artifact '${options.artifact}' is not canonical for '${options.family}'` };
501
415
  }
502
416
  return { ok: true, qualification: task.qualification, audit: audit.audit };
@@ -509,19 +423,18 @@ function renderQualificationAudit(audit: QualificationAudit): string {
509
423
  ...rows.map((row) => `| ${row.map((value) => value.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ')).join(' | ')} |`)
510
424
  ].join('\n');
511
425
  const list = (values: readonly string[]) => values.join(',');
512
- const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: '', upstreamArtifactDigest: upstreamArtifactDigest(audit.upstreamRelations) };
426
+ const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: '' };
513
427
  return [
514
428
  '### 约束依赖', '', table(DEPENDENCY_COLUMNS, audit.constraintDependencies.map((row) => [row.constraintId, row.constraintDigest, row.role, row.evidence])), '',
515
429
  '### 候选资格', '', table(QUALIFICATION_COLUMNS, audit.candidateQualifications.map((row) => [row.candidateId, row.status, row.impact, list(row.constraintIds), row.evidence])), '',
516
430
  '### 分类结果', '', table(CLASSIFICATION_COLUMNS, audit.classifications.map((row) => [row.decisionId, row.classification, row.evidence])), '',
517
- '### 上游关系', '', table(RELATION_COLUMNS, audit.upstreamRelations.map((row) => [row.upstreamFamily, row.upstreamArtifact, String(row.upstreamRound), row.upstreamSha256, row.relation])), '',
518
- '### 依赖快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest, snapshot.upstreamArtifactDigest]])
431
+ '### 资格快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest]])
519
432
  ].join('\n');
520
433
  }
521
434
 
522
435
  function buildQualificationAudit(
523
436
  taskContent: string,
524
- input: { constraints?: readonly string[]; candidates?: readonly string[]; classifications?: readonly QualificationClassificationRow[]; upstreamRelations?: readonly UpstreamRelation[] } = {}
437
+ input: { constraints?: readonly string[]; candidates?: readonly string[]; classifications?: readonly QualificationClassificationRow[] } = {}
525
438
  ): { ok: true; audit: QualificationAudit } | { ok: false; code: string; message: string } {
526
439
  const parsed = parseTaskQualification(taskContent);
527
440
  if (!parsed.ok) return parsed;
@@ -530,13 +443,12 @@ function buildQualificationAudit(
530
443
  const candidates = input.candidates ?? parsed.qualification.candidates.map((row) => row.candidateId);
531
444
  const constraintDependencies = parsed.qualification.constraints.filter((row) => ids.has(row.constraintId)).map((row) => ({ constraintId: row.constraintId, constraintDigest: row.digest, role: 'required' as const, evidence: row.evidence }));
532
445
  const candidateQualifications = parsed.qualification.candidates.filter((row) => candidates.includes(row.candidateId)).map((row) => ({ candidateId: row.candidateId, status: row.status, impact: row.impact, constraintIds: row.constraintIds, evidence: row.evidence }));
533
- const upstreamRelations = [...(input.upstreamRelations ?? [])];
534
446
  return {
535
447
  ok: true,
536
448
  audit: {
537
449
  present: true, constraintDependencies, candidateQualifications,
538
- classifications: input.classifications ?? [], upstreamRelations,
539
- snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest, upstreamArtifactDigest: upstreamArtifactDigest(upstreamRelations) }
450
+ classifications: input.classifications ?? [],
451
+ snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest }
540
452
  }
541
453
  };
542
454
  }
@@ -552,10 +464,8 @@ export {
552
464
  DEPENDENCY_COLUMNS,
553
465
  QUALIFICATION_COLUMNS,
554
466
  CLASSIFICATION_COLUMNS,
555
- RELATION_COLUMNS,
556
467
  SNAPSHOT_COLUMNS,
557
468
  constraintDigest,
558
- expectedQualificationRelations,
559
469
  buildQualificationAudit,
560
470
  nonConstraintInputDigest,
561
471
  parseQualificationAudit,
@@ -563,7 +473,6 @@ export {
563
473
  parseTaskQualification,
564
474
  renderQualificationAudit,
565
475
  taskInputDigest,
566
- upstreamArtifactDigest,
567
476
  validateQualificationAudit
568
477
  };
569
478
  export type {
@@ -578,11 +487,9 @@ export type {
578
487
  QualificationConfirmation,
579
488
  QualificationClassification,
580
489
  QualificationClassificationRow,
581
- QualificationRelation,
582
490
  QualificationRole,
583
491
  QualificationValidationError,
584
492
  QualificationValidationResult,
585
493
  TaskQualification,
586
- UpstreamRelation,
587
494
  ArtifactFamily
588
495
  };
@@ -13,6 +13,7 @@ import { validateLifecycleExecution } from './lifecycle-execution.ts';
13
13
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
14
14
  import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
15
15
  import { getArtifactSchema } from './artifact-schema.ts';
16
+ import { validateQualificationAudit } from './qualification-audit.ts';
16
17
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
17
18
 
18
19
  type ReviewFinalizationErrorCode =
@@ -258,6 +259,12 @@ function prepareReviewSummaryCandidate(
258
259
  artifactSha256, semanticDigest: structure.semanticDigest
259
260
  });
260
261
  }
262
+ const qualification = validateQualificationAudit(taskContent, artifactContent, { family: spec.family, artifact: request.artifact });
263
+ if (!qualification.ok) {
264
+ return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', `${qualification.code}: ${qualification.message}`, {
265
+ artifactSha256, semanticDigest
266
+ });
267
+ }
261
268
  const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
262
269
  refreshNumericCounts: true
263
270
  });
@@ -0,0 +1,85 @@
1
+ import fs from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+
5
+ import { resolveBranchWorktree } from '../git/branch-worktree.ts';
6
+ import { snapshotReview } from '../git/review-snapshot.ts';
7
+ import { resolveDeliveryTarget } from './delivery-target.ts';
8
+ import { parseTypedTaskFrontmatter } from './frontmatter.ts';
9
+ import {
10
+ extractReviewDiffBase,
11
+ extractReviewDiffFingerprint,
12
+ extractReviewedHead,
13
+ extractReviewTargetHead,
14
+ extractReviewedSnapshotTree,
15
+ loadPostReviewConfig,
16
+ resolvePostReviewGlobs
17
+ } from './review-fingerprint.ts';
18
+
19
+ export type ReviewIdentityResult = Readonly<{
20
+ status: 'matched' | 'different' | 'indeterminate';
21
+ message: string;
22
+ reviewedHead: string | null;
23
+ currentHead: string | null;
24
+ }>;
25
+
26
+ function git(cwd: string, args: string[]): string {
27
+ return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' }).trim();
28
+ }
29
+
30
+ function result(
31
+ status: ReviewIdentityResult['status'], message: string,
32
+ reviewedHead: string | null = null, currentHead: string | null = null
33
+ ): ReviewIdentityResult {
34
+ return { status, message, reviewedHead, currentHead };
35
+ }
36
+
37
+ export function inspectReviewIdentity(taskDir: string, reportContent: string, repositoryRoot?: string): ReviewIdentityResult {
38
+ const reviewedHead = extractReviewedHead(reportContent);
39
+ const targetHead = extractReviewTargetHead(reportContent);
40
+ const diffBase = extractReviewDiffBase(reportContent);
41
+ const fingerprint = extractReviewDiffFingerprint(reportContent);
42
+ const reviewedTree = extractReviewedSnapshotTree(reportContent);
43
+ if (!reviewedHead || !targetHead || !diffBase || !fingerprint || !reviewedTree) {
44
+ return result('indeterminate', 'Review report is missing commit or snapshot identity', reviewedHead || null);
45
+ }
46
+ try {
47
+ const metadata = parseTypedTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'));
48
+ const taskRepositoryRoot = git(taskDir, ['rev-parse', '--show-toplevel']);
49
+ const branch = String(metadata.branch ?? '').trim();
50
+ const gitRoot = branch ? resolveBranchWorktree(taskRepositoryRoot, branch) : taskRepositoryRoot;
51
+ if (!gitRoot) return result('indeterminate', `Task branch '${branch}' is not checked out in a registered worktree`, reviewedHead);
52
+ const currentHead = git(gitRoot, ['rev-parse', 'HEAD']);
53
+ const baseline = git(gitRoot, ['rev-parse', `${reviewedHead}^{commit}`]);
54
+ if (baseline !== currentHead) return result('different', 'Reviewed commit differs from current HEAD', baseline, currentHead);
55
+
56
+ const remote = String(metadata.delivery_remote ?? '').trim();
57
+ const baseRef = String(metadata.delivery_base_ref ?? '').trim();
58
+ if (!remote || !baseRef) return result('indeterminate', 'Task delivery target is not bound', baseline, currentHead);
59
+ const target = resolveDeliveryTarget(gitRoot, { remote, baseRef });
60
+ if (!target.ok) return result('indeterminate', target.message, baseline, currentHead);
61
+ const currentTarget = git(gitRoot, ['rev-parse', `${targetHead}^{commit}`]);
62
+ const computedDiffBase = git(gitRoot, ['merge-base', baseline, currentTarget]);
63
+ if (git(gitRoot, ['rev-parse', `${diffBase}^{commit}`]) !== computedDiffBase) {
64
+ return result('different', 'Saved review diff base no longer matches the current target', baseline, currentHead);
65
+ }
66
+
67
+ const globs = resolvePostReviewGlobs({}, loadPostReviewConfig(repositoryRoot ?? taskRepositoryRoot));
68
+ const trackedChanges = git(gitRoot, ['diff', '--name-only', '-z', baseline, '--', ...globs]);
69
+ const untrackedChanges = git(gitRoot, ['ls-files', '-o', '--exclude-standard', '-z', '--', ...globs]);
70
+ if (trackedChanges || untrackedChanges) return result('different', 'Reviewed worktree contains uncommitted changes', baseline, currentHead);
71
+
72
+ const snapshot = snapshotReview({
73
+ cwd: gitRoot,
74
+ mode: 'worktree',
75
+ baseline,
76
+ diffBase: computedDiffBase,
77
+ globs
78
+ });
79
+ if (snapshot.fingerprint !== fingerprint) return result('different', 'Reviewed diff fingerprint differs from the current worktree', baseline, currentHead);
80
+ if (snapshot.tree !== reviewedTree) return result('different', 'Reviewed snapshot tree differs from the current worktree', baseline, currentHead);
81
+ return result('matched', 'Review report matches the current commit and clean worktree', baseline, currentHead);
82
+ } catch (error) {
83
+ return result('indeterminate', error instanceof Error ? error.message : String(error), reviewedHead);
84
+ }
85
+ }
@@ -74,7 +74,7 @@ class DocumentMutationError extends Error {
74
74
 
75
75
  function parseTable(
76
76
  content: string,
77
- input: { sectionAliases: readonly string[]; columns: readonly string[]; keyColumn?: string }
77
+ input: { sectionAliases: readonly string[]; columns: readonly string[]; keyColumn?: string | null }
78
78
  ): ParsedTable | null {
79
79
  validateAliases(input.sectionAliases, 'table section aliases');
80
80
  if (
@@ -84,8 +84,8 @@ function parseTable(
84
84
  ) {
85
85
  throw new DocumentMutationError('MUTATION_INVALID', 'table columns are invalid');
86
86
  }
87
- const keyColumn = input.keyColumn ?? input.columns[0]!;
88
- if (!input.columns.includes(keyColumn)) {
87
+ const keyColumn = input.keyColumn === undefined ? input.columns[0]! : input.keyColumn;
88
+ if (keyColumn !== null && !input.columns.includes(keyColumn)) {
89
89
  throw new DocumentMutationError('MUTATION_INVALID', 'table key column is invalid');
90
90
  }
91
91
  const sections = matchingSections(content, input.sectionAliases);
@@ -132,11 +132,11 @@ function parseTable(
132
132
  column,
133
133
  decodeCell(parsed.cells[columnIndex]!).trim()
134
134
  ]));
135
- const key = values[keyColumn]!;
136
- if (seen.has(key)) {
135
+ const key = keyColumn === null ? null : values[keyColumn]!;
136
+ if (key !== null && seen.has(key)) {
137
137
  throw new DocumentMutationError('TABLE_DUPLICATE_KEY', `duplicate table key '${key}'`);
138
138
  }
139
- seen.add(key);
139
+ if (key !== null) seen.add(key);
140
140
  rows.push({ values, sourceLine: index - Math.max(firstContent, 0) });
141
141
  }
142
142
  return { heading: section.heading, rows };
@@ -27,6 +27,7 @@ type LinkDisposition = 'exists' | 'missing' | TaskExecutionLockError;
27
27
 
28
28
  type TaskExecutionLockOptions = Readonly<{
29
29
  lockRoot?: string;
30
+ identityScope?: string;
30
31
  token?: () => string;
31
32
  now?: () => string;
32
33
  getStartTime?: (pid: number) => number | null;
@@ -74,9 +75,12 @@ function mapLinkError(operation: LinkOperation, error: unknown, key: string): Li
74
75
  );
75
76
  }
76
77
 
77
- function lockKey(repoRoot: string, taskId: string): Readonly<{ canonicalRepoRoot: string; key: string }> {
78
+ function lockKey(repoRoot: string, taskId: string, identityScope?: string): Readonly<{ canonicalRepoRoot: string; key: string }> {
78
79
  const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
79
- const key = createHash('sha256').update(`${canonicalRepoRoot}\0${taskId}`).digest('hex');
80
+ const identity = identityScope === undefined
81
+ ? `${canonicalRepoRoot}\0${taskId}`
82
+ : `${identityScope}\0${taskId}`;
83
+ const key = createHash('sha256').update(identity).digest('hex');
80
84
  return { canonicalRepoRoot, key };
81
85
  }
82
86
 
@@ -190,7 +194,7 @@ function withTaskExecutionLock<T>(
190
194
  ): T {
191
195
  let identity: Readonly<{ canonicalRepoRoot: string; key: string }>;
192
196
  try {
193
- identity = lockKey(repoRoot, taskId);
197
+ identity = lockKey(repoRoot, taskId, options.identityScope);
194
198
  } catch (error) {
195
199
  throw new TaskExecutionLockError(
196
200
  'ORCHESTRATION_LOCK_FAILED',
@@ -6,12 +6,9 @@ import { execFileSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  import {
9
- extractReviewBaseline,
10
9
  extractReviewTargetHead,
11
10
  extractReviewedHead,
12
11
  extractReviewDiffBase,
13
- extractReviewDiffFingerprint,
14
- extractReviewedSnapshotTree,
15
12
  findAuthoritativeReviewCodeArtifact,
16
13
  loadPostReviewConfig,
17
14
  parseReviewVerdict,
@@ -23,7 +20,6 @@ import { inspectPlatformPullRequest } from "../platform/pull-requests.ts";
23
20
  import { resolveMaterializedReviewedHeadRelation } from "../platform/change-request-git-evidence.ts";
24
21
  import { resolveReviewedHeadRelation } from "../platform/merged-pr-equivalence.ts";
25
22
  import { resolveLocalReviewedCommitRelation } from "../git/reviewed-commit-equivalence.ts";
26
- import { resolveBranchWorktree } from "../git/branch-worktree.ts";
27
23
  import { parseTypedTaskFrontmatter } from "./frontmatter.ts";
28
24
  import { LEDGER_TERMINAL, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from "./ledger.ts";
29
25
  import type { LedgerRow } from "./ledger.ts";
@@ -33,12 +29,10 @@ import { inspectDecisionDetailDuplicates } from "./decision-details.ts";
33
29
  import { parseImplementationInputs, type ImplementationInput } from "./implementation-inputs.ts";
34
30
  import { scanVisibleMarkdown } from "./markdown.ts";
35
31
  import { loadVerificationConfig } from "./verification-config.ts";
36
- import { snapshotReview } from "../git/review-snapshot.ts";
37
32
  import { inspectActivityLog } from "./activity-log.ts";
38
33
  import { parseWorkflowWarnings } from "./workflow-warnings.ts";
39
34
  import { OrchestrationStateError, readRun } from "./orchestration.ts";
40
35
  import type { OrchestrationRun } from "./orchestration.ts";
41
- import { resolveDeliveryTarget } from "./delivery-target.ts";
42
36
  import { readPrDeliveryFact } from "./pr-delivery-fact.ts";
43
37
  import { validateLocalArtifact } from "./local-artifact-finalization.ts";
44
38
  import { getArtifactSchema } from "./artifact-schema.ts";
@@ -50,6 +44,8 @@ import { readManualValidationCompletion } from "./manual-validation-completion.t
50
44
  import { sha256File } from "./artifact-receipts.ts";
51
45
  import { summaryCommentState } from "../platform/pr-summary.ts";
52
46
  import { taskIssueIdentity } from "../platform/task-identities.ts";
47
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
48
+ import { inspectReviewIdentity } from './review-identity.ts';
53
49
 
54
50
  const TASK_ENUMS = {
55
51
  type: ["feature", "bugfix", "refactor", "docs", "chore"],
@@ -1159,12 +1155,16 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }: any): any {
1159
1155
  }
1160
1156
 
1161
1157
  const content = fs.readFileSync(resolvedArtifact.path, "utf8");
1158
+ if (!hasArtifactCompletionFact(task.content, resolvedArtifact.path, 'review-code.completed')) {
1159
+ return failResult("review-fact", `Review Code completion fact does not match ${path.basename(resolvedArtifact.path)}`);
1160
+ }
1161
+ if (!hasArtifactCompletionLog(task.content, path.basename(resolvedArtifact.path), 'review-code.completed')) {
1162
+ return failResult("review-fact", `Review Code completion entry does not reference ${path.basename(resolvedArtifact.path)}`);
1163
+ }
1162
1164
  const verdict = parseReviewVerdict(content);
1163
1165
  const reviewTargetHead = extractReviewTargetHead(content);
1164
1166
  const reviewReviewedHead = extractReviewedHead(content);
1165
1167
  const reviewDiffBase = extractReviewDiffBase(content);
1166
- const reviewedFingerprint = extractReviewDiffFingerprint(content);
1167
- const reviewedTree = extractReviewedSnapshotTree(content);
1168
1168
 
1169
1169
  if (!["通过", "需要修改", "拒绝", "Approved", "Changes Requested", "Rejected"].includes(verdict)) {
1170
1170
  return failResult("review-fact", `Unsupported review verdict '${verdict}'`);
@@ -1179,86 +1179,20 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }: any): any {
1179
1179
  return failResult("review-fact", "Review fact must record target head, reviewed head, and diff base");
1180
1180
  }
1181
1181
 
1182
- let gitRoot;
1183
- let head;
1184
- let baseline;
1185
- let targetHead;
1186
- let diffBase;
1187
- try {
1188
- const taskRepositoryRoot = execFileSync("git", ["-C", taskDir, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
1189
- gitRoot = reviewWorktreeForTask(taskRepositoryRoot, String(task.metadata.branch || "").trim()) ?? taskRepositoryRoot;
1190
- head = execFileSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
1191
- const target = resolveDeliveryTarget(gitRoot, { remote: deliveryRemote, baseRef: deliveryBaseRef });
1192
- if (!target.ok) throw new Error(target.message);
1193
- const reviewedRef = reviewReviewedHead;
1194
- baseline = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewedRef}^{commit}`], { encoding: "utf8" }).trim();
1195
- targetHead = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewTargetHead}^{commit}`], { encoding: "utf8" }).trim();
1196
- const computed = execFileSync("git", ["-C", gitRoot, "merge-base", baseline, targetHead], { encoding: "utf8" }).trim();
1197
- diffBase = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewDiffBase}^{commit}`], { encoding: "utf8" }).trim();
1198
- if (diffBase !== computed) throw new Error('saved review diff base does not match merge-base(reviewed head, target head)');
1199
- } catch {
1200
- return blockedResult(
1201
- "review-fact",
1202
- `Unable to resolve saved review commits in the task repository; re-run review-code`
1203
- );
1204
- }
1205
-
1206
- if (baseline !== head) {
1207
- return failResult(
1208
- "review-fact",
1209
- `Reviewed head ${baseline.slice(0, 8)} does not match current HEAD ${head.slice(0, 8)}; re-run review-code`
1210
- );
1211
- }
1212
-
1213
- let actualSnapshot;
1214
- try {
1215
- actualSnapshot = snapshotReview({
1216
- cwd: gitRoot,
1217
- mode: "worktree",
1218
- baseline,
1219
- diffBase,
1220
- globs: resolvePostReviewGlobs({}, loadPostReviewConfig(repositoryRoot))
1221
- });
1222
- } catch {
1223
- return blockedResult(
1224
- "review-fact",
1225
- `Unable to recompute reviewed diff fingerprint from baseline ${baseline.slice(0, 8)}; re-run review-code`
1226
- );
1227
- }
1228
-
1229
- if (actualSnapshot.fingerprint !== reviewedFingerprint) {
1230
- return failResult(
1231
- "review-fact",
1232
- `Reviewed diff fingerprint does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`
1233
- );
1234
- }
1235
-
1236
- if (!reviewedTree || actualSnapshot.tree !== reviewedTree) {
1237
- return failResult(
1238
- "review-fact",
1239
- `Reviewed snapshot tree does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`
1240
- );
1182
+ const identity = inspectReviewIdentity(taskDir, content, repositoryRoot);
1183
+ if (identity.status !== 'matched' || !identity.reviewedHead) {
1184
+ return failResult('review-fact', `Review identity is ${identity.status}: ${identity.message}; re-run review-code`);
1241
1185
  }
1186
+ const baseline = identity.reviewedHead;
1242
1187
 
1243
1188
  if (["通过", "Approved"].includes(verdict)) {
1244
1189
  const lastReviewedCommit = String(task.metadata.last_reviewed_commit || "").trim();
1245
- const cleanSnapshot = actualSnapshot.tree === execFileSync(
1246
- "git",
1247
- ["-C", gitRoot, "rev-parse", `${baseline}^{tree}`],
1248
- { encoding: "utf8" }
1249
- ).trim();
1250
- if (cleanSnapshot && lastReviewedCommit !== baseline) {
1190
+ if (lastReviewedCommit !== baseline) {
1251
1191
  return failResult(
1252
1192
  "review-fact",
1253
1193
  `Approved clean review must set task last_reviewed_commit to reviewed head ${baseline.slice(0, 8)}`
1254
1194
  );
1255
1195
  }
1256
- if (!cleanSnapshot && lastReviewedCommit) {
1257
- return failResult(
1258
- "review-fact",
1259
- "Approved review with uncommitted changes must remain unanchored until commit"
1260
- );
1261
- }
1262
1196
  }
1263
1197
 
1264
1198
  return passResult(
@@ -1267,13 +1201,6 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }: any): any {
1267
1201
  );
1268
1202
  }
1269
1203
 
1270
- function reviewWorktreeForTask(repositoryRoot: string, branch: string): string | null {
1271
- if (!branch) return null;
1272
- const worktree = resolveBranchWorktree(repositoryRoot, branch);
1273
- if (worktree) return worktree;
1274
- throw new Error(`Task branch '${branch}' is not checked out in a registered worktree`);
1275
- }
1276
-
1277
1204
  function resolvePostReviewBaseline({ gitRoot, lastReviewedCommit, reviewArtifact }: any): any {
1278
1205
  if (SHA_PATTERN.test(lastReviewedCommit) && gitCommitExists(gitRoot, lastReviewedCommit)) {
1279
1206
  return { ok: true, sha: lastReviewedCommit };