@fitlab-ai/agent-infra 0.11.6 → 0.11.7

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 (115) hide show
  1. package/dist/lib/git/primary-worktree.js +58 -0
  2. package/dist/lib/platform/pull-requests.js +29 -3
  3. package/dist/lib/sandbox/control/server.js +6 -2
  4. package/dist/lib/server/config.js +7 -27
  5. package/dist/lib/server/daemon.js +4 -3
  6. package/dist/lib/server/process-control.js +4 -2
  7. package/dist/lib/server/runner.js +4 -3
  8. package/dist/lib/task/artifact-lifecycle.js +42 -10
  9. package/dist/lib/task/artifact-receipts.js +62 -22
  10. package/dist/lib/task/capabilities.js +22 -4
  11. package/dist/lib/task/completion-facts.js +56 -0
  12. package/dist/lib/task/create-service.js +6 -2
  13. package/dist/lib/task/create.js +46 -82
  14. package/dist/lib/task/events.js +268 -195
  15. package/dist/lib/task/finalization-handoff.js +2 -1
  16. package/dist/lib/task/invalidation-command.js +4 -2
  17. package/dist/lib/task/invalidation.js +38 -9
  18. package/dist/lib/task/local-artifact-finalization.js +17 -3
  19. package/dist/lib/task/qualification-audit.js +23 -105
  20. package/dist/lib/task/review-finalization.js +7 -0
  21. package/dist/lib/task/review-identity.js +69 -0
  22. package/dist/lib/task/sections.js +6 -5
  23. package/dist/lib/task/task-execution-lock.js +6 -3
  24. package/dist/lib/task/verification-engine.js +14 -63
  25. package/dist/lib/task/verification.js +35 -38
  26. package/dist/lifecycle-build-manifest.json +3 -3
  27. package/lib/git/primary-worktree.ts +62 -0
  28. package/lib/platform/pull-requests.ts +27 -3
  29. package/lib/sandbox/control/server.ts +5 -2
  30. package/lib/server/config.ts +7 -29
  31. package/lib/server/daemon.ts +5 -3
  32. package/lib/server/process-control.ts +4 -2
  33. package/lib/server/runner.ts +7 -4
  34. package/lib/server/server.schema.json +0 -4
  35. package/lib/task/artifact-lifecycle.ts +35 -11
  36. package/lib/task/artifact-receipts.ts +55 -15
  37. package/lib/task/capabilities.ts +17 -3
  38. package/lib/task/completion-facts.ts +66 -0
  39. package/lib/task/create-service.ts +7 -3
  40. package/lib/task/create.ts +44 -71
  41. package/lib/task/events.ts +248 -182
  42. package/lib/task/finalization-handoff.ts +2 -1
  43. package/lib/task/invalidation-command.ts +4 -2
  44. package/lib/task/invalidation.ts +35 -10
  45. package/lib/task/local-artifact-finalization.ts +13 -3
  46. package/lib/task/qualification-audit.ts +21 -114
  47. package/lib/task/review-finalization.ts +7 -0
  48. package/lib/task/review-identity.ts +85 -0
  49. package/lib/task/sections.ts +6 -6
  50. package/lib/task/task-execution-lock.ts +7 -3
  51. package/lib/task/verification-engine.ts +13 -86
  52. package/lib/task/verification.ts +34 -38
  53. package/package.json +1 -1
  54. package/templates/.agents/rules/README.en.md +1 -0
  55. package/templates/.agents/rules/README.zh-CN.md +1 -0
  56. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  57. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  58. package/templates/.agents/rules/validation-output.en.md +19 -0
  59. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  60. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  61. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  62. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  63. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  64. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -0
  65. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  66. package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
  67. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  68. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  69. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  70. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  71. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  72. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  73. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  74. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  75. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  76. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  77. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  78. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  79. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  80. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  81. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  82. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  83. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  84. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  85. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  86. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  87. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  88. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  89. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  90. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  91. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  92. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  93. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  94. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  95. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  96. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  97. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  98. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  99. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  100. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  101. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  102. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  103. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  104. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  105. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  106. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -0
  107. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -0
  108. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  109. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  110. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  111. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  112. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  113. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  114. package/templates/.agents/templates/task.en.md +2 -2
  115. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { parseArtifactName } from './artifact-name.js';
2
3
  import { findSectionRange, parseTable } from './sections.js';
3
4
  import { scanVisibleMarkdown } from './markdown.js';
4
5
  const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'];
@@ -7,6 +8,10 @@ const OPERATION_COLUMNS = [
7
8
  'status', 'processed', 'total', 'created_at', 'updated_at', 'completed_at', 'error'
8
9
  ];
9
10
  const TARGET_COLUMNS = [
11
+ 'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact', 'target_input',
12
+ 'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
13
+ ];
14
+ const LEGACY_TARGET_COLUMNS = [
10
15
  'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact',
11
16
  'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
12
17
  ];
@@ -20,7 +25,7 @@ function operationIdFor(source) {
20
25
  return `INV-${hashIdentity([source.sourceFamily, source.sourceArtifact, source.sourceRound, source.sourceSha256])}`;
21
26
  }
22
27
  function targetIdFor(operationId, target) {
23
- return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, target.targetRound, target.targetSha256])}`;
28
+ return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, ...(target.targetKind === 'receipt' && target.targetInput ? [target.targetInput] : []), target.targetRound, target.targetSha256])}`;
24
29
  }
25
30
  function createInvalidationOperation(source, targets = []) {
26
31
  const operationId = source.operationId ?? operationIdFor(source);
@@ -31,14 +36,28 @@ function createInvalidationOperation(source, targets = []) {
31
36
  createdAt: source.createdAt, updatedAt: source.updatedAt, completedAt: '', error: ''
32
37
  };
33
38
  }
34
- function parseInvalidationTable(body, heading, columns) {
39
+ function parseInvalidationTable(body, heading, columnChoices) {
35
40
  const section = findSectionRange(body, [heading], 3);
36
41
  const lines = scanVisibleMarkdown(body).lines.filter((line) => line.text.trim());
37
- const table = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
42
+ let table = null;
43
+ let legacy = false;
44
+ for (const columns of columnChoices) {
45
+ try {
46
+ const candidate = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
47
+ if (candidate) {
48
+ table = candidate;
49
+ legacy = columns === LEGACY_TARGET_COLUMNS;
50
+ break;
51
+ }
52
+ }
53
+ catch {
54
+ // Try the next explicitly supported schema.
55
+ }
56
+ }
38
57
  if (!section || !table || lines.filter((line) => line.start >= section.bodyStart && line.start < section.end).length !== table.rows.length + 2) {
39
58
  throw new Error(`invalidation ${heading} section must contain only its canonical table`);
40
59
  }
41
- return table.rows;
60
+ return { rows: table.rows, legacy };
42
61
  }
43
62
  function required(value, field) {
44
63
  if (!value)
@@ -62,9 +81,10 @@ function parseInvalidationDocument(content) {
62
81
  return { ok: true, present: false, document: { operations: [], targets: [] } };
63
82
  try {
64
83
  const body = content.slice(section.bodyStart, section.end);
65
- const operationRows = parseInvalidationTable(body, 'Operations', OPERATION_COLUMNS);
66
- const targetRows = parseInvalidationTable(body, 'Targets', TARGET_COLUMNS);
67
- const operations = operationRows.map(({ values: row }) => ({
84
+ const operationRows = parseInvalidationTable(body, 'Operations', [OPERATION_COLUMNS]);
85
+ // TODO(compat): Remove legacy target table parsing and completed receipt targets without target_input once a workspace inventory confirms all persisted targets have input identities.
86
+ const targetRows = parseInvalidationTable(body, 'Targets', [TARGET_COLUMNS, LEGACY_TARGET_COLUMNS]);
87
+ const operations = operationRows.rows.map(({ values: row }) => ({
68
88
  operationId: required(row.operation_id, 'operation_id'), sourceFamily: required(row.source_family, 'source_family'),
69
89
  sourceArtifact: required(row.source_artifact, 'source_artifact'), sourceRound: numberValue(row.source_round, 'source_round', false),
70
90
  sourceSha256: required(row.source_sha256, 'source_sha256'), status: status(row.status, 'operation status'),
@@ -72,10 +92,11 @@ function parseInvalidationDocument(content) {
72
92
  createdAt: required(row.created_at, 'created_at'), updatedAt: required(row.updated_at, 'updated_at'),
73
93
  completedAt: row.completed_at ?? '', error: row.error ?? ''
74
94
  }));
75
- const targets = targetRows.map(({ values: row }) => ({
95
+ const targets = targetRows.rows.map(({ values: row }) => ({
76
96
  targetId: required(row.target_id, 'target_id'), operationId: required(row.operation_id, 'operation_id'),
77
97
  targetKind: row.target_kind, targetFamily: required(row.target_family, 'target_family'),
78
98
  targetArtifact: required(row.target_artifact, 'target_artifact'), targetRound: numberValue(row.target_round, 'target_round', false),
99
+ ...(row.target_input ? { targetInput: row.target_input } : {}),
79
100
  targetSha256: required(row.target_sha256, 'target_sha256'), status: status(row.status, 'target status'),
80
101
  reasonCode: required(row.reason_code, 'reason_code'), updatedAt: required(row.updated_at, 'updated_at')
81
102
  }));
@@ -100,6 +121,14 @@ function parseInvalidationDocument(content) {
100
121
  targetIds.add(target.targetId);
101
122
  if (!operationIds.has(target.operationId))
102
123
  throw new Error(`target '${target.targetId}' references an unknown operation`);
124
+ if (target.targetKind === 'receipt') {
125
+ if (!target.targetInput && target.status !== 'completed')
126
+ throw new Error(`receipt target '${target.targetId}' input identity is required`);
127
+ if (target.targetInput && !parseArtifactName(target.targetInput))
128
+ throw new Error(`receipt target '${target.targetId}' input identity is invalid`);
129
+ }
130
+ else if (target.targetInput)
131
+ throw new Error(`non-receipt target '${target.targetId}' cannot have an input identity`);
103
132
  if (target.targetId !== targetIdFor(target.operationId, target))
104
133
  throw new Error(`target '${target.targetId}' has a non-canonical identity`);
105
134
  if (!/^[a-f0-9]{64}$/.test(target.targetSha256))
@@ -143,7 +172,7 @@ function renderInvalidation(document) {
143
172
  '### Targets', '',
144
173
  table(TARGET_COLUMNS, targets.map((row) => [
145
174
  row.targetId, row.operationId, row.targetKind, row.targetFamily, row.targetArtifact,
146
- row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
175
+ row.targetInput ?? '', row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
147
176
  ]))
148
177
  ].join('\n');
149
178
  }
@@ -6,6 +6,7 @@ import { parseLifecyclePathDecision } from './lifecycle-path.js';
6
6
  import { resolveTaskRef } from './resolve-ref.js';
7
7
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
8
8
  import { getArtifactSchema } from './artifact-schema.js';
9
+ import { validateQualificationAudit } from './qualification-audit.js';
9
10
  const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
10
11
  analysis: getArtifactSchema('analysis').sections.map((section) => section.headings.zh),
11
12
  plan: getArtifactSchema('plan').sections.map((section) => section.headings.zh),
@@ -82,19 +83,32 @@ function prepareLocalArtifact(request, candidate) {
82
83
  const validation = validateLocalArtifact(content, {
83
84
  family: request.family, requiredSections: request.requiredSections
84
85
  });
86
+ let qualificationError = null;
87
+ try {
88
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
89
+ const qualification = validateQualificationAudit(taskContent, content, { family: request.family, artifact: request.artifact });
90
+ if (!qualification.ok)
91
+ qualificationError = `${qualification.code}: ${qualification.message}`;
92
+ }
93
+ catch (error) {
94
+ qualificationError = `QUALIFICATION_AUDIT_INVALID: ${String(error)}`;
95
+ }
85
96
  const artifactSha256 = sha256Content(content);
97
+ const diagnostics = qualificationError
98
+ ? [...validation.diagnostics, { code: 'LOCAL_STRUCTURAL_INVALID', message: qualificationError, line: null }]
99
+ : validation.diagnostics;
86
100
  const result = failedFinalization(request, {
87
101
  code: 'LOCAL_ARTIFACT_INVALID',
88
- message: validation.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
102
+ message: diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
89
103
  }, {
90
104
  taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256,
91
- semanticDigest: validation.semanticDigest, diagnostics: validation.diagnostics
105
+ semanticDigest: validation.semanticDigest, diagnostics
92
106
  });
93
107
  // Trusted local operators edit the canonical artifact directly. A
94
108
  // finalization result is a fresh observation of those bytes, never a
95
109
  // capability minted by an earlier recovery attempt or generation.
96
110
  return {
97
- result: validation.ok ? { ...result, status: 'passed', error: null } : result,
111
+ result: validation.ok && !qualificationError ? { ...result, status: 'passed', error: null } : result,
98
112
  content,
99
113
  repoRoot: resolved.repoRoot,
100
114
  lockAlreadyHeld: request.lockAlreadyHeld
@@ -1,30 +1,26 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { parseArtifactName } from './artifact-name.js';
3
- import { parseTypedTaskFrontmatter } from './frontmatter.js';
4
3
  import { findSectionRange, parseTable } from './sections.js';
5
4
  import { scanVisibleMarkdown } from './markdown.js';
6
5
  const TASK_CONSTRAINT_HEADINGS = ['约束', 'Constraints'];
7
6
  const TASK_CANDIDATE_HEADINGS = ['候选与否决方案', 'Candidate and Rejected Options', 'Candidates and Rejected Options', 'Candidates and Rejected Alternatives'];
8
7
  const AUDIT_HEADINGS = ['资格审计', '资格审计复核', 'Qualification Audit', 'Qualification Audit Review'];
9
8
  const CONFIRMATION_HEADINGS = ['资格确认记录', 'Qualification Confirmations'];
10
- const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '上游关系', '依赖快照'];
9
+ const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '资格快照'];
11
10
  const AUDIT_SUBSECTION_HEADINGS = {
12
11
  '约束依赖': ['约束依赖', 'Constraint Dependencies'],
13
12
  '候选资格': ['候选资格', 'Candidate Qualification'],
14
13
  '分类结果': ['分类结果', 'Classification Results'],
15
- '上游关系': ['上游关系', 'Upstream Relations'],
16
- '依赖快照': ['依赖快照', 'Dependency Snapshot']
14
+ '资格快照': ['资格快照', 'Qualification Snapshot']
17
15
  };
18
16
  const CONSTRAINT_COLUMNS = ['constraint_id', 'statement', 'status', 'authority', 'source', 'evidence', 'derived_from', 'approval_evidence'];
19
17
  const CANDIDATE_COLUMNS = ['candidate_id', 'statement', 'status', 'constraint_ids', 'impact', 'evidence'];
20
18
  const DEPENDENCY_COLUMNS = ['constraint_id', 'constraint_digest', 'role', 'evidence'];
21
19
  const QUALIFICATION_COLUMNS = ['candidate_id', 'status', 'impact', 'constraint_ids', 'evidence'];
22
20
  const CLASSIFICATION_COLUMNS = ['decision_id', 'classification', 'evidence'];
23
- const RELATION_COLUMNS = ['upstream_family', 'upstream_artifact', 'upstream_round', 'upstream_sha256', 'relation'];
24
- const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest', 'upstream_artifact_digest'];
21
+ const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest'];
25
22
  const CONFIRMATION_COLUMNS = ['qcr_id', 'constraint_id', 'actor', 'entrypoint', 'request_id', 'approved_digest', 'confirmed_at', 'rationale'];
26
23
  const ARTIFACT_FAMILIES = ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'];
27
- const RELATIONS = ['required-input', 'reviewed-input', 'review-context', 'approval-context'];
28
24
  const CONSTRAINT_STATUSES = ['confirmed', 'derived', 'assumption', 'open', 'conflicted', 'superseded'];
29
25
  const CANDIDATE_STATUSES = ['qualified', 'rejected', 'pending', 'needs-human-decision', 'confirmed', 'excluded'];
30
26
  const CLASSIFICATIONS = ['deterministic', 'qualified', 'rejected', 'needs-human-decision', 'not-applicable'];
@@ -215,86 +211,20 @@ function parseAuditTable(content, heading, columns) {
215
211
  const table = parseTable(`## Qualification Audit Body\n${subsection}`, { sectionAliases: ['Qualification Audit Body'], columns });
216
212
  return table?.rows.map((row) => ({ ...row.values })) ?? null;
217
213
  }
218
- function parseAuditArtifactName(value) {
219
- const identity = parseArtifactName(value);
220
- return identity && ARTIFACT_FAMILIES.includes(identity.family)
221
- ? { family: identity.family, round: identity.round } : null;
222
- }
223
- function upstreamArtifactDigest(rows) {
224
- return digest([...rows].map((row) => ({ ...row })).sort((a, b) => `${a.upstreamFamily}/${a.upstreamArtifact}/${a.relation}`.localeCompare(`${b.upstreamFamily}/${b.upstreamArtifact}/${b.relation}`)));
225
- }
226
- function validateUpstreamRelation(row) {
227
- const identity = parseAuditArtifactName(row.upstreamArtifact);
228
- if (!ARTIFACT_FAMILIES.includes(row.upstreamFamily) || !identity || identity.family !== row.upstreamFamily
229
- || identity.round !== row.upstreamRound || !Number.isSafeInteger(row.upstreamRound)
230
- || !/^[a-f0-9]{64}$/i.test(row.upstreamSha256) || !RELATIONS.includes(row.relation)) {
231
- throw new Error(`invalid qualification upstream relation '${row.upstreamArtifact}'`);
232
- }
233
- }
234
- function expectedQualificationRelations(taskContent, family) {
235
- let frontmatter;
236
- try {
237
- frontmatter = parseTypedTaskFrontmatter(taskContent);
238
- }
239
- catch (error) {
240
- return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
241
- }
242
- const encoded = frontmatter.qualification_input_relations;
243
- if (typeof encoded === 'string' && encoded) {
244
- try {
245
- const parsed = JSON.parse(encoded);
246
- if (!Array.isArray(parsed))
247
- throw new Error('qualification_input_relations must be an array');
248
- const relations = parsed.map((value) => {
249
- if (!value || typeof value !== 'object' || Array.isArray(value))
250
- throw new Error('qualification input relation must be an object');
251
- const row = value;
252
- const relation = {
253
- upstreamFamily: row.upstreamFamily,
254
- upstreamArtifact: String(row.upstreamArtifact ?? ''),
255
- upstreamRound: Number(row.upstreamRound),
256
- upstreamSha256: String(row.upstreamSha256 ?? ''),
257
- relation: row.relation
258
- };
259
- validateUpstreamRelation(relation);
260
- return relation;
261
- });
262
- return { ok: true, relations };
263
- }
264
- catch (error) {
265
- return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
266
- }
267
- }
268
- const inputName = family === 'code' ? frontmatter.code_input_artifact : family.startsWith('review-') ? frontmatter.review_input_artifact : '';
269
- const inputSha256 = family === 'code' ? frontmatter.code_input_sha256 : family.startsWith('review-') ? frontmatter.review_input_sha256 : '';
270
- if (typeof inputName !== 'string' || !inputName || typeof inputSha256 !== 'string' || !inputSha256)
271
- return { ok: true, relations: undefined };
272
- const identity = parseAuditArtifactName(inputName);
273
- if (!identity)
274
- return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: `started input '${inputName}' is not canonical` };
275
- const relation = {
276
- upstreamFamily: identity.family, upstreamArtifact: inputName, upstreamRound: identity.round,
277
- upstreamSha256: inputSha256, relation: family === 'code' ? 'required-input' : 'reviewed-input'
278
- };
279
- try {
280
- validateUpstreamRelation(relation);
281
- }
282
- catch (error) {
283
- return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
284
- }
285
- return { ok: true, relations: [relation] };
286
- }
287
214
  function parseQualificationAudit(content) {
288
- if (sectionBody(content, AUDIT_HEADINGS) === null)
289
- return { ok: true, audit: { present: false, constraintDependencies: [], candidateQualifications: [], classifications: [], upstreamRelations: [], snapshot: null } };
215
+ const auditBody = sectionBody(content, AUDIT_HEADINGS);
216
+ if (auditBody === null)
217
+ return { ok: true, audit: { present: false, constraintDependencies: [], candidateQualifications: [], classifications: [], snapshot: null } };
290
218
  try {
219
+ if (sectionBody(auditBody, ['上游关系', 'Upstream Relations'], 3) !== null) {
220
+ throw new Error('qualification audit uses the obsolete lifecycle-relation format; regenerate it using the current format');
221
+ }
291
222
  const dependencies = parseAuditTable(content, '约束依赖', DEPENDENCY_COLUMNS);
292
223
  const qualifications = parseAuditTable(content, '候选资格', QUALIFICATION_COLUMNS);
293
224
  const classifications = parseAuditTable(content, '分类结果', CLASSIFICATION_COLUMNS);
294
- const relations = parseAuditTable(content, '上游关系', RELATION_COLUMNS);
295
- const snapshots = parseAuditTable(content, '依赖快照', SNAPSHOT_COLUMNS);
296
- if (!dependencies || !qualifications || !classifications || !relations || !snapshots || snapshots.length !== 1) {
297
- throw new Error('qualification audit requires all five canonical tables and exactly one dependency snapshot');
225
+ const snapshots = parseAuditTable(content, '资格快照', SNAPSHOT_COLUMNS);
226
+ if (!dependencies || !qualifications || !classifications || !snapshots || snapshots.length !== 1) {
227
+ throw new Error('qualification audit requires three decision tables and exactly one qualification snapshot');
298
228
  }
299
229
  const constraintDependencies = dependencies.map((row) => {
300
230
  if (!/^C-[1-9]\d*$/.test(row.constraint_id ?? '') || !/^[a-f0-9]{64}$/i.test(row.constraint_digest ?? '') || !ROLES.includes(row.role) || !row.evidence?.trim())
@@ -311,20 +241,15 @@ function parseQualificationAudit(content) {
311
241
  throw new Error(`invalid qualification classification '${row.decision_id ?? ''}'`);
312
242
  return { decisionId: row.decision_id, classification: row.classification, evidence: row.evidence };
313
243
  });
314
- const upstreamRelations = relations.map((row) => {
315
- const relation = { upstreamFamily: row.upstream_family, upstreamArtifact: row.upstream_artifact ?? '', upstreamRound: Number(row.upstream_round), upstreamSha256: row.upstream_sha256 ?? '', relation: row.relation };
316
- validateUpstreamRelation(relation);
317
- return relation;
318
- });
319
244
  const snapshotRow = snapshots[0];
320
- 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 ?? ''))
321
- throw new Error('qualification dependency snapshot has invalid digest');
245
+ if (!/^[a-f0-9]{64}$/i.test(snapshotRow.task_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.non_constraint_input_digest ?? ''))
246
+ throw new Error('qualification snapshot has invalid digest');
322
247
  return {
323
248
  ok: true,
324
249
  audit: {
325
250
  present: true, constraintDependencies, candidateQualifications,
326
- classifications: classificationRows, upstreamRelations,
327
- snapshot: { taskInputDigest: snapshotRow.task_input_digest, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest, upstreamArtifactDigest: snapshotRow.upstream_artifact_digest }
251
+ classifications: classificationRows,
252
+ snapshot: { taskInputDigest: snapshotRow.task_input_digest, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest }
328
253
  }
329
254
  };
330
255
  }
@@ -367,7 +292,7 @@ function validateQualificationAudit(taskContent, artifactContent, options = {})
367
292
  const audit = parseQualificationAudit(artifactContent);
368
293
  if (!audit.ok)
369
294
  return audit;
370
- if (!task.qualification.present && !options.require)
295
+ if (!audit.audit.present && !options.require)
371
296
  return { ok: true, qualification: task.qualification, audit: audit.audit };
372
297
  if (!task.qualification.present)
373
298
  return { ok: false, code: 'QUALIFICATION_TASK_CONTRACT_MISSING', message: 'task qualification contract is missing' };
@@ -424,13 +349,8 @@ function validateQualificationAudit(taskContent, artifactContent, options = {})
424
349
  return { ok: false, code: 'QUALIFICATION_TASK_DIGEST_MISMATCH', message: 'qualification audit task input digest is stale' };
425
350
  if (snapshot.nonConstraintInputDigest !== task.qualification.nonConstraintInputDigest)
426
351
  return { ok: false, code: 'QUALIFICATION_NON_CONSTRAINT_DIGEST_MISMATCH', message: 'qualification audit non-constraint input digest is stale' };
427
- if (snapshot.upstreamArtifactDigest !== upstreamArtifactDigest(audit.audit.upstreamRelations))
428
- return { ok: false, code: 'QUALIFICATION_UPSTREAM_DIGEST_MISMATCH', message: 'qualification audit upstream digest does not match its relation rows' };
429
- if (options.expectedUpstreamRelations && (options.expectedUpstreamRelations.length !== audit.audit.upstreamRelations.length
430
- || upstreamArtifactDigest(options.expectedUpstreamRelations) !== upstreamArtifactDigest(audit.audit.upstreamRelations)))
431
- return { ok: false, code: 'QUALIFICATION_UPSTREAM_RELATION_MISMATCH', message: 'qualification audit upstream relations do not match the started artifact inputs' };
432
352
  if (options.family && options.artifact) {
433
- const identity = parseAuditArtifactName(options.artifact);
353
+ const identity = parseArtifactName(options.artifact);
434
354
  if (!identity || identity.family !== options.family)
435
355
  return { ok: false, code: 'QUALIFICATION_ARTIFACT_IDENTITY_INVALID', message: `artifact '${options.artifact}' is not canonical for '${options.family}'` };
436
356
  }
@@ -443,13 +363,12 @@ function renderQualificationAudit(audit) {
443
363
  ...rows.map((row) => `| ${row.map((value) => value.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ')).join(' | ')} |`)
444
364
  ].join('\n');
445
365
  const list = (values) => values.join(',');
446
- const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: '', upstreamArtifactDigest: upstreamArtifactDigest(audit.upstreamRelations) };
366
+ const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: '' };
447
367
  return [
448
368
  '### 约束依赖', '', table(DEPENDENCY_COLUMNS, audit.constraintDependencies.map((row) => [row.constraintId, row.constraintDigest, row.role, row.evidence])), '',
449
369
  '### 候选资格', '', table(QUALIFICATION_COLUMNS, audit.candidateQualifications.map((row) => [row.candidateId, row.status, row.impact, list(row.constraintIds), row.evidence])), '',
450
370
  '### 分类结果', '', table(CLASSIFICATION_COLUMNS, audit.classifications.map((row) => [row.decisionId, row.classification, row.evidence])), '',
451
- '### 上游关系', '', table(RELATION_COLUMNS, audit.upstreamRelations.map((row) => [row.upstreamFamily, row.upstreamArtifact, String(row.upstreamRound), row.upstreamSha256, row.relation])), '',
452
- '### 依赖快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest, snapshot.upstreamArtifactDigest]])
371
+ '### 资格快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest]])
453
372
  ].join('\n');
454
373
  }
455
374
  function buildQualificationAudit(taskContent, input = {}) {
@@ -462,15 +381,14 @@ function buildQualificationAudit(taskContent, input = {}) {
462
381
  const candidates = input.candidates ?? parsed.qualification.candidates.map((row) => row.candidateId);
463
382
  const constraintDependencies = parsed.qualification.constraints.filter((row) => ids.has(row.constraintId)).map((row) => ({ constraintId: row.constraintId, constraintDigest: row.digest, role: 'required', evidence: row.evidence }));
464
383
  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 }));
465
- const upstreamRelations = [...(input.upstreamRelations ?? [])];
466
384
  return {
467
385
  ok: true,
468
386
  audit: {
469
387
  present: true, constraintDependencies, candidateQualifications,
470
- classifications: input.classifications ?? [], upstreamRelations,
471
- snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest, upstreamArtifactDigest: upstreamArtifactDigest(upstreamRelations) }
388
+ classifications: input.classifications ?? [],
389
+ snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest }
472
390
  }
473
391
  };
474
392
  }
475
- export { ARTIFACT_FAMILIES, AUDIT_HEADINGS, AUDIT_SUBSECTIONS, CONFIRMATION_COLUMNS, CONFIRMATION_HEADINGS, CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, DEPENDENCY_COLUMNS, QUALIFICATION_COLUMNS, CLASSIFICATION_COLUMNS, RELATION_COLUMNS, SNAPSHOT_COLUMNS, constraintDigest, expectedQualificationRelations, buildQualificationAudit, nonConstraintInputDigest, parseQualificationAudit, parseQualificationConfirmations, parseTaskQualification, renderQualificationAudit, taskInputDigest, upstreamArtifactDigest, validateQualificationAudit };
393
+ export { ARTIFACT_FAMILIES, AUDIT_HEADINGS, AUDIT_SUBSECTIONS, CONFIRMATION_COLUMNS, CONFIRMATION_HEADINGS, CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, DEPENDENCY_COLUMNS, QUALIFICATION_COLUMNS, CLASSIFICATION_COLUMNS, SNAPSHOT_COLUMNS, constraintDigest, buildQualificationAudit, nonConstraintInputDigest, parseQualificationAudit, parseQualificationConfirmations, parseTaskQualification, renderQualificationAudit, taskInputDigest, validateQualificationAudit };
476
394
  //# sourceMappingURL=qualification-audit.js.map
@@ -10,6 +10,7 @@ import { resolveTaskRef } from './resolve-ref.js';
10
10
  import { validateLifecycleExecution } from './lifecycle-execution.js';
11
11
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
12
12
  import { getArtifactSchema } from './artifact-schema.js';
13
+ import { validateQualificationAudit } from './qualification-audit.js';
13
14
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
14
15
  const STAGES = {
15
16
  analysis: { family: 'review-analysis' },
@@ -186,6 +187,12 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
186
187
  artifactSha256, semanticDigest: structure.semanticDigest
187
188
  });
188
189
  }
190
+ const qualification = validateQualificationAudit(taskContent, artifactContent, { family: spec.family, artifact: request.artifact });
191
+ if (!qualification.ok) {
192
+ return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', `${qualification.code}: ${qualification.message}`, {
193
+ artifactSha256, semanticDigest
194
+ });
195
+ }
189
196
  const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
190
197
  refreshNumericCounts: true
191
198
  });
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { resolveBranchWorktree } from '../git/branch-worktree.js';
5
+ import { snapshotReview } from '../git/review-snapshot.js';
6
+ import { resolveDeliveryTarget } from './delivery-target.js';
7
+ import { parseTypedTaskFrontmatter } from './frontmatter.js';
8
+ import { extractReviewDiffBase, extractReviewDiffFingerprint, extractReviewedHead, extractReviewTargetHead, extractReviewedSnapshotTree, loadPostReviewConfig, resolvePostReviewGlobs } from './review-fingerprint.js';
9
+ function git(cwd, args) {
10
+ return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' }).trim();
11
+ }
12
+ function result(status, message, reviewedHead = null, currentHead = null) {
13
+ return { status, message, reviewedHead, currentHead };
14
+ }
15
+ export function inspectReviewIdentity(taskDir, reportContent, repositoryRoot) {
16
+ const reviewedHead = extractReviewedHead(reportContent);
17
+ const targetHead = extractReviewTargetHead(reportContent);
18
+ const diffBase = extractReviewDiffBase(reportContent);
19
+ const fingerprint = extractReviewDiffFingerprint(reportContent);
20
+ const reviewedTree = extractReviewedSnapshotTree(reportContent);
21
+ if (!reviewedHead || !targetHead || !diffBase || !fingerprint || !reviewedTree) {
22
+ return result('indeterminate', 'Review report is missing commit or snapshot identity', reviewedHead || null);
23
+ }
24
+ try {
25
+ const metadata = parseTypedTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'));
26
+ const taskRepositoryRoot = git(taskDir, ['rev-parse', '--show-toplevel']);
27
+ const branch = String(metadata.branch ?? '').trim();
28
+ const gitRoot = branch ? resolveBranchWorktree(taskRepositoryRoot, branch) : taskRepositoryRoot;
29
+ if (!gitRoot)
30
+ return result('indeterminate', `Task branch '${branch}' is not checked out in a registered worktree`, reviewedHead);
31
+ const currentHead = git(gitRoot, ['rev-parse', 'HEAD']);
32
+ const baseline = git(gitRoot, ['rev-parse', `${reviewedHead}^{commit}`]);
33
+ if (baseline !== currentHead)
34
+ return result('different', 'Reviewed commit differs from current HEAD', baseline, currentHead);
35
+ const remote = String(metadata.delivery_remote ?? '').trim();
36
+ const baseRef = String(metadata.delivery_base_ref ?? '').trim();
37
+ if (!remote || !baseRef)
38
+ return result('indeterminate', 'Task delivery target is not bound', baseline, currentHead);
39
+ const target = resolveDeliveryTarget(gitRoot, { remote, baseRef });
40
+ if (!target.ok)
41
+ return result('indeterminate', target.message, baseline, currentHead);
42
+ const currentTarget = git(gitRoot, ['rev-parse', `${targetHead}^{commit}`]);
43
+ const computedDiffBase = git(gitRoot, ['merge-base', baseline, currentTarget]);
44
+ if (git(gitRoot, ['rev-parse', `${diffBase}^{commit}`]) !== computedDiffBase) {
45
+ return result('different', 'Saved review diff base no longer matches the current target', baseline, currentHead);
46
+ }
47
+ const globs = resolvePostReviewGlobs({}, loadPostReviewConfig(repositoryRoot ?? taskRepositoryRoot));
48
+ const trackedChanges = git(gitRoot, ['diff', '--name-only', '-z', baseline, '--', ...globs]);
49
+ const untrackedChanges = git(gitRoot, ['ls-files', '-o', '--exclude-standard', '-z', '--', ...globs]);
50
+ if (trackedChanges || untrackedChanges)
51
+ return result('different', 'Reviewed worktree contains uncommitted changes', baseline, currentHead);
52
+ const snapshot = snapshotReview({
53
+ cwd: gitRoot,
54
+ mode: 'worktree',
55
+ baseline,
56
+ diffBase: computedDiffBase,
57
+ globs
58
+ });
59
+ if (snapshot.fingerprint !== fingerprint)
60
+ return result('different', 'Reviewed diff fingerprint differs from the current worktree', baseline, currentHead);
61
+ if (snapshot.tree !== reviewedTree)
62
+ return result('different', 'Reviewed snapshot tree differs from the current worktree', baseline, currentHead);
63
+ return result('matched', 'Review report matches the current commit and clean worktree', baseline, currentHead);
64
+ }
65
+ catch (error) {
66
+ return result('indeterminate', error instanceof Error ? error.message : String(error), reviewedHead);
67
+ }
68
+ }
69
+ //# sourceMappingURL=review-identity.js.map
@@ -17,8 +17,8 @@ function parseTable(content, input) {
17
17
  new Set(input.columns).size !== input.columns.length) {
18
18
  throw new DocumentMutationError('MUTATION_INVALID', 'table columns are invalid');
19
19
  }
20
- const keyColumn = input.keyColumn ?? input.columns[0];
21
- if (!input.columns.includes(keyColumn)) {
20
+ const keyColumn = input.keyColumn === undefined ? input.columns[0] : input.keyColumn;
21
+ if (keyColumn !== null && !input.columns.includes(keyColumn)) {
22
22
  throw new DocumentMutationError('MUTATION_INVALID', 'table key column is invalid');
23
23
  }
24
24
  const sections = matchingSections(content, input.sectionAliases);
@@ -68,11 +68,12 @@ function parseTable(content, input) {
68
68
  column,
69
69
  decodeCell(parsed.cells[columnIndex]).trim()
70
70
  ]));
71
- const key = values[keyColumn];
72
- if (seen.has(key)) {
71
+ const key = keyColumn === null ? null : values[keyColumn];
72
+ if (key !== null && seen.has(key)) {
73
73
  throw new DocumentMutationError('TABLE_DUPLICATE_KEY', `duplicate table key '${key}'`);
74
74
  }
75
- seen.add(key);
75
+ if (key !== null)
76
+ seen.add(key);
76
77
  rows.push({ values, sourceLine: index - Math.max(firstContent, 0) });
77
78
  }
78
79
  return { heading: section.heading, rows };
@@ -29,9 +29,12 @@ function mapLinkError(operation, error, key) {
29
29
  }
30
30
  return new TaskExecutionLockError('ORCHESTRATION_LOCK_FAILED', `lifecycle lock hard-link operation '${operation}' failed`, detail);
31
31
  }
32
- function lockKey(repoRoot, taskId) {
32
+ function lockKey(repoRoot, taskId, identityScope) {
33
33
  const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
34
- const key = createHash('sha256').update(`${canonicalRepoRoot}\0${taskId}`).digest('hex');
34
+ const identity = identityScope === undefined
35
+ ? `${canonicalRepoRoot}\0${taskId}`
36
+ : `${identityScope}\0${taskId}`;
37
+ const key = createHash('sha256').update(identity).digest('hex');
35
38
  return { canonicalRepoRoot, key };
36
39
  }
37
40
  function parseOwner(raw, key) {
@@ -133,7 +136,7 @@ function cleanStaleCandidates(lockRoot, key, identityMatches) {
133
136
  function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options = {}) {
134
137
  let identity;
135
138
  try {
136
- identity = lockKey(repoRoot, taskId);
139
+ identity = lockKey(repoRoot, taskId, options.identityScope);
137
140
  }
138
141
  catch (error) {
139
142
  throw new TaskExecutionLockError('ORCHESTRATION_LOCK_FAILED', 'failed to canonicalize the repository for lifecycle locking', { operation: 'canonicalize', errno: errno(error), key: 'unresolved' });