@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.
- package/dist/bin/internal-cli.js +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/git/primary-worktree.js +58 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +125 -7
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +11 -5
- package/dist/lib/task/create.js +60 -79
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/defaults.json +6 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +132 -8
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +12 -6
- package/lib/task/create.ts +54 -69
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- 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 = ['约束依赖', '候选资格', '分类结果', '
|
|
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
|
-
'
|
|
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
|
|
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
|
-
|
|
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
|
|
378
|
-
|
|
379
|
-
|
|
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 ?? '')
|
|
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,
|
|
407
|
-
snapshot: { taskInputDigest: snapshotRow.task_input_digest!, nonConstraintInputDigest: snapshotRow.non_constraint_input_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 (!
|
|
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 =
|
|
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: ''
|
|
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
|
-
'###
|
|
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[]
|
|
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 ?? [],
|
|
539
|
-
snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest
|
|
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
|
+
}
|
package/lib/task/sections.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
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 };
|