@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.
- package/dist/lib/git/primary-worktree.js +58 -0
- package/dist/lib/platform/pull-requests.js +29 -3
- package/dist/lib/sandbox/control/server.js +6 -2
- 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/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 +6 -2
- package/dist/lib/task/create.js +46 -82
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- 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/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/sandbox/control/server.ts +5 -2
- 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/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 +7 -3
- package/lib/task/create.ts +44 -71
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/local-artifact-finalization.ts +13 -3
- 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/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- 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/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 +2 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- 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 +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -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/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 +2 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -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/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
|
@@ -4,14 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { extractReviewTargetHead, extractReviewedHead, extractReviewDiffBase, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from "./verification-review.js";
|
|
8
8
|
import { check as checkPlatformSync } from "../platform/verification-sync.js";
|
|
9
9
|
import { check as checkRequiredChecks } from "../platform/verification-required.js";
|
|
10
10
|
import { inspectPlatformPullRequest } from "../platform/pull-requests.js";
|
|
11
11
|
import { resolveMaterializedReviewedHeadRelation } from "../platform/change-request-git-evidence.js";
|
|
12
12
|
import { resolveReviewedHeadRelation } from "../platform/merged-pr-equivalence.js";
|
|
13
13
|
import { resolveLocalReviewedCommitRelation } from "../git/reviewed-commit-equivalence.js";
|
|
14
|
-
import { resolveBranchWorktree } from "../git/branch-worktree.js";
|
|
15
14
|
import { parseTypedTaskFrontmatter } from "./frontmatter.js";
|
|
16
15
|
import { LEDGER_TERMINAL, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from "./ledger.js";
|
|
17
16
|
import { isValidAgentInfraVersion } from "../version.js";
|
|
@@ -20,11 +19,9 @@ import { inspectDecisionDetailDuplicates } from "./decision-details.js";
|
|
|
20
19
|
import { parseImplementationInputs } from "./implementation-inputs.js";
|
|
21
20
|
import { scanVisibleMarkdown } from "./markdown.js";
|
|
22
21
|
import { loadVerificationConfig } from "./verification-config.js";
|
|
23
|
-
import { snapshotReview } from "../git/review-snapshot.js";
|
|
24
22
|
import { inspectActivityLog } from "./activity-log.js";
|
|
25
23
|
import { parseWorkflowWarnings } from "./workflow-warnings.js";
|
|
26
24
|
import { OrchestrationStateError, readRun } from "./orchestration.js";
|
|
27
|
-
import { resolveDeliveryTarget } from "./delivery-target.js";
|
|
28
25
|
import { readPrDeliveryFact } from "./pr-delivery-fact.js";
|
|
29
26
|
import { validateLocalArtifact } from "./local-artifact-finalization.js";
|
|
30
27
|
import { getArtifactSchema } from "./artifact-schema.js";
|
|
@@ -35,6 +32,8 @@ import { readManualValidationCompletion } from "./manual-validation-completion.j
|
|
|
35
32
|
import { sha256File } from "./artifact-receipts.js";
|
|
36
33
|
import { summaryCommentState } from "../platform/pr-summary.js";
|
|
37
34
|
import { taskIssueIdentity } from "../platform/task-identities.js";
|
|
35
|
+
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
|
|
36
|
+
import { inspectReviewIdentity } from './review-identity.js';
|
|
38
37
|
const TASK_ENUMS = {
|
|
39
38
|
type: ["feature", "bugfix", "refactor", "docs", "chore"],
|
|
40
39
|
workflow: ["feature-development", "bug-fix", "refactoring"],
|
|
@@ -1014,12 +1013,16 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }) {
|
|
|
1014
1013
|
return failResult("review-fact", task.message);
|
|
1015
1014
|
}
|
|
1016
1015
|
const content = fs.readFileSync(resolvedArtifact.path, "utf8");
|
|
1016
|
+
if (!hasArtifactCompletionFact(task.content, resolvedArtifact.path, 'review-code.completed')) {
|
|
1017
|
+
return failResult("review-fact", `Review Code completion fact does not match ${path.basename(resolvedArtifact.path)}`);
|
|
1018
|
+
}
|
|
1019
|
+
if (!hasArtifactCompletionLog(task.content, path.basename(resolvedArtifact.path), 'review-code.completed')) {
|
|
1020
|
+
return failResult("review-fact", `Review Code completion entry does not reference ${path.basename(resolvedArtifact.path)}`);
|
|
1021
|
+
}
|
|
1017
1022
|
const verdict = parseReviewVerdict(content);
|
|
1018
1023
|
const reviewTargetHead = extractReviewTargetHead(content);
|
|
1019
1024
|
const reviewReviewedHead = extractReviewedHead(content);
|
|
1020
1025
|
const reviewDiffBase = extractReviewDiffBase(content);
|
|
1021
|
-
const reviewedFingerprint = extractReviewDiffFingerprint(content);
|
|
1022
|
-
const reviewedTree = extractReviewedSnapshotTree(content);
|
|
1023
1026
|
if (!["通过", "需要修改", "拒绝", "Approved", "Changes Requested", "Rejected"].includes(verdict)) {
|
|
1024
1027
|
return failResult("review-fact", `Unsupported review verdict '${verdict}'`);
|
|
1025
1028
|
}
|
|
@@ -1031,71 +1034,19 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }) {
|
|
|
1031
1034
|
if (!reviewTargetHead || !reviewReviewedHead || !reviewDiffBase) {
|
|
1032
1035
|
return failResult("review-fact", "Review fact must record target head, reviewed head, and diff base");
|
|
1033
1036
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
let targetHead;
|
|
1038
|
-
let diffBase;
|
|
1039
|
-
try {
|
|
1040
|
-
const taskRepositoryRoot = execFileSync("git", ["-C", taskDir, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
1041
|
-
gitRoot = reviewWorktreeForTask(taskRepositoryRoot, String(task.metadata.branch || "").trim()) ?? taskRepositoryRoot;
|
|
1042
|
-
head = execFileSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
1043
|
-
const target = resolveDeliveryTarget(gitRoot, { remote: deliveryRemote, baseRef: deliveryBaseRef });
|
|
1044
|
-
if (!target.ok)
|
|
1045
|
-
throw new Error(target.message);
|
|
1046
|
-
const reviewedRef = reviewReviewedHead;
|
|
1047
|
-
baseline = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewedRef}^{commit}`], { encoding: "utf8" }).trim();
|
|
1048
|
-
targetHead = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewTargetHead}^{commit}`], { encoding: "utf8" }).trim();
|
|
1049
|
-
const computed = execFileSync("git", ["-C", gitRoot, "merge-base", baseline, targetHead], { encoding: "utf8" }).trim();
|
|
1050
|
-
diffBase = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewDiffBase}^{commit}`], { encoding: "utf8" }).trim();
|
|
1051
|
-
if (diffBase !== computed)
|
|
1052
|
-
throw new Error('saved review diff base does not match merge-base(reviewed head, target head)');
|
|
1053
|
-
}
|
|
1054
|
-
catch {
|
|
1055
|
-
return blockedResult("review-fact", `Unable to resolve saved review commits in the task repository; re-run review-code`);
|
|
1056
|
-
}
|
|
1057
|
-
if (baseline !== head) {
|
|
1058
|
-
return failResult("review-fact", `Reviewed head ${baseline.slice(0, 8)} does not match current HEAD ${head.slice(0, 8)}; re-run review-code`);
|
|
1059
|
-
}
|
|
1060
|
-
let actualSnapshot;
|
|
1061
|
-
try {
|
|
1062
|
-
actualSnapshot = snapshotReview({
|
|
1063
|
-
cwd: gitRoot,
|
|
1064
|
-
mode: "worktree",
|
|
1065
|
-
baseline,
|
|
1066
|
-
diffBase,
|
|
1067
|
-
globs: resolvePostReviewGlobs({}, loadPostReviewConfig(repositoryRoot))
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
catch {
|
|
1071
|
-
return blockedResult("review-fact", `Unable to recompute reviewed diff fingerprint from baseline ${baseline.slice(0, 8)}; re-run review-code`);
|
|
1072
|
-
}
|
|
1073
|
-
if (actualSnapshot.fingerprint !== reviewedFingerprint) {
|
|
1074
|
-
return failResult("review-fact", `Reviewed diff fingerprint does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`);
|
|
1075
|
-
}
|
|
1076
|
-
if (!reviewedTree || actualSnapshot.tree !== reviewedTree) {
|
|
1077
|
-
return failResult("review-fact", `Reviewed snapshot tree does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`);
|
|
1037
|
+
const identity = inspectReviewIdentity(taskDir, content, repositoryRoot);
|
|
1038
|
+
if (identity.status !== 'matched' || !identity.reviewedHead) {
|
|
1039
|
+
return failResult('review-fact', `Review identity is ${identity.status}: ${identity.message}; re-run review-code`);
|
|
1078
1040
|
}
|
|
1041
|
+
const baseline = identity.reviewedHead;
|
|
1079
1042
|
if (["通过", "Approved"].includes(verdict)) {
|
|
1080
1043
|
const lastReviewedCommit = String(task.metadata.last_reviewed_commit || "").trim();
|
|
1081
|
-
|
|
1082
|
-
if (cleanSnapshot && lastReviewedCommit !== baseline) {
|
|
1044
|
+
if (lastReviewedCommit !== baseline) {
|
|
1083
1045
|
return failResult("review-fact", `Approved clean review must set task last_reviewed_commit to reviewed head ${baseline.slice(0, 8)}`);
|
|
1084
1046
|
}
|
|
1085
|
-
if (!cleanSnapshot && lastReviewedCommit) {
|
|
1086
|
-
return failResult("review-fact", "Approved review with uncommitted changes must remain unanchored until commit");
|
|
1087
|
-
}
|
|
1088
1047
|
}
|
|
1089
1048
|
return passResult("review-fact", `Review fact valid for ${path.basename(resolvedArtifact.path)} at ${baseline.slice(0, 8)}`);
|
|
1090
1049
|
}
|
|
1091
|
-
function reviewWorktreeForTask(repositoryRoot, branch) {
|
|
1092
|
-
if (!branch)
|
|
1093
|
-
return null;
|
|
1094
|
-
const worktree = resolveBranchWorktree(repositoryRoot, branch);
|
|
1095
|
-
if (worktree)
|
|
1096
|
-
return worktree;
|
|
1097
|
-
throw new Error(`Task branch '${branch}' is not checked out in a registered worktree`);
|
|
1098
|
-
}
|
|
1099
1050
|
function resolvePostReviewBaseline({ gitRoot, lastReviewedCommit, reviewArtifact }) {
|
|
1100
1051
|
if (SHA_PATTERN.test(lastReviewedCommit) && gitCommitExists(gitRoot, lastReviewedCommit)) {
|
|
1101
1052
|
return { ok: true, sha: lastReviewedCommit };
|
|
@@ -89,47 +89,44 @@ async function verifyTaskEvent(request, options = {}) {
|
|
|
89
89
|
function statusLabel(status) {
|
|
90
90
|
return status === 'fail' ? 'FAIL' : status === 'blocked' ? 'BLOCKED' : 'pass';
|
|
91
91
|
}
|
|
92
|
-
function renderPayload(payload) {
|
|
93
|
-
const lines = [];
|
|
94
|
-
if (Array.isArray(payload.checks)) {
|
|
95
|
-
const gateMode = typeof payload.gate === 'string';
|
|
96
|
-
lines.push(gateMode
|
|
97
|
-
? `Verification: ${payload.gate} | Skill: ${payload.skill}`
|
|
98
|
-
: `Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
|
|
99
|
-
for (const check of payload.checks) {
|
|
100
|
-
const raw = statusLabel(check.status);
|
|
101
|
-
const effective = statusLabel(check.effectiveStatus ?? check.status);
|
|
102
|
-
const reason = check.reason ?? 'CHECK_FAILED';
|
|
103
|
-
const message = check.message ?? reason;
|
|
104
|
-
lines.push(` [${effective}] ${check.checkId ?? check.type} (${check.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${message}; ${check.action ?? 'Review validation output'}`);
|
|
105
|
-
}
|
|
106
|
-
const summary = payload.summary ?? (() => {
|
|
107
|
-
const checks = payload.checks;
|
|
108
|
-
const statuses = checks.map((check) => check.effectiveStatus ?? check.status);
|
|
109
|
-
return `${statuses.filter((status) => status === 'pass').length} passed, ${statuses.filter((status) => status === 'fail').length} failed${statuses.includes('blocked') ? `, ${statuses.filter((status) => status === 'blocked').length} blocked` : ''}`;
|
|
110
|
-
})();
|
|
111
|
-
lines.push('', `Result: ${summary} - ${payload.action}`);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
lines.push(`Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
|
|
115
|
-
const raw = statusLabel(payload.status);
|
|
116
|
-
const effective = statusLabel(payload.effectiveStatus ?? payload.status);
|
|
117
|
-
const reason = payload.reason ?? 'CHECK_FAILED';
|
|
118
|
-
lines.push(` [${effective}] ${payload.checkId ?? payload.type} (${payload.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${payload.message ?? reason}; ${payload.action ?? 'Review validation output'}`);
|
|
119
|
-
const status = payload.effectiveStatus ?? payload.status;
|
|
120
|
-
const summary = status === 'pass' ? '1 passed, 0 failed'
|
|
121
|
-
: status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
|
|
122
|
-
const action = status === 'pass' ? 'Requested check passed'
|
|
123
|
-
: status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
|
|
124
|
-
: `Fix ${payload.type} issues and re-run check`;
|
|
125
|
-
lines.push('', `Result: ${summary} - ${action}`);
|
|
126
|
-
}
|
|
127
|
-
return `${lines.join('\n')}\n`;
|
|
128
|
-
}
|
|
129
92
|
function renderTaskVerification(result) {
|
|
130
93
|
if (result.status === 'failed')
|
|
131
94
|
return `Verification failed: ${result.error?.code ?? 'VERIFY_FAILED'} - ${result.error?.message ?? 'unknown error'}\n`;
|
|
132
|
-
|
|
95
|
+
const target = `${result.event}${result.artifact ? ` (${result.artifact})` : ''}`;
|
|
96
|
+
const lines = [`Verification: ${result.status} | Target: ${target} | Skill: ${result.skill}`];
|
|
97
|
+
const checks = result.invocations.flatMap(({ payload }) => {
|
|
98
|
+
if (Array.isArray(payload.checks))
|
|
99
|
+
return payload.checks;
|
|
100
|
+
return [payload];
|
|
101
|
+
});
|
|
102
|
+
const counts = {
|
|
103
|
+
pass: checks.filter((check) => (check.effectiveStatus ?? check.status) === 'pass').length,
|
|
104
|
+
fail: checks.filter((check) => (check.effectiveStatus ?? check.status) === 'fail').length,
|
|
105
|
+
blocked: checks.filter((check) => (check.effectiveStatus ?? check.status) === 'blocked').length
|
|
106
|
+
};
|
|
107
|
+
const summary = `${counts.pass} passed, ${counts.fail} failed${counts.blocked > 0 ? `, ${counts.blocked} blocked` : ''}`;
|
|
108
|
+
lines.push('', `Result: ${summary}`);
|
|
109
|
+
for (const check of checks) {
|
|
110
|
+
const rawStatus = check.status;
|
|
111
|
+
const effectiveStatus = check.effectiveStatus ?? rawStatus;
|
|
112
|
+
const isSoftWarning = (rawStatus === 'fail' || rawStatus === 'blocked') && effectiveStatus === 'pass';
|
|
113
|
+
const reason = check.reason ?? 'CHECK_FAILED';
|
|
114
|
+
const message = check.message ?? reason;
|
|
115
|
+
const action = check.action ?? 'Review validation output';
|
|
116
|
+
const id = check.checkId ?? check.type;
|
|
117
|
+
const isHumanExemptionNotice = id === 'post-review-commit'
|
|
118
|
+
&& typeof message === 'string'
|
|
119
|
+
&& /\bhuman-decided (?:post-review )?exemption\b/i.test(message)
|
|
120
|
+
&& /\bPRC-\d+\s*:/.test(message);
|
|
121
|
+
if (effectiveStatus === 'pass' && !isSoftWarning && !isHumanExemptionNotice)
|
|
122
|
+
continue;
|
|
123
|
+
lines.push(isSoftWarning
|
|
124
|
+
? `Warning: ${id} (${rawStatus === 'blocked' ? `raw BLOCKED; ` : ''}${reason}) - ${message}; ${action}`
|
|
125
|
+
: isHumanExemptionNotice
|
|
126
|
+
? `Notice: ${id} - ${message}; ${action}`
|
|
127
|
+
: ` [${statusLabel(effectiveStatus)}] ${id} (${reason}) - ${message}; ${action}`);
|
|
128
|
+
}
|
|
129
|
+
return `${lines.join('\n')}\n`;
|
|
133
130
|
}
|
|
134
131
|
export { VERIFICATION_CATALOG, renderTaskVerification, verifyTaskEvent };
|
|
135
132
|
//# sourceMappingURL=verification.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocolVersion": 3,
|
|
3
|
-
"packageVersion": "0.11.
|
|
4
|
-
"sourceInputHash": "
|
|
5
|
-
"internalExecutableBuildHash": "
|
|
3
|
+
"packageVersion": "0.11.7",
|
|
4
|
+
"sourceInputHash": "095f0cf30aa29fcd2dd45123dc755e5097d7a9f81de33420461fd943cff122b2",
|
|
5
|
+
"internalExecutableBuildHash": "a956f2ed366e025a111a87bc1f542b87d83d4025e068304f95b1aae704709d3f"
|
|
6
6
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { sameFilesystemEntry } from './worktree-identity.ts';
|
|
5
|
+
|
|
6
|
+
function gitText(repositoryRoot: string, args: string[]): string {
|
|
7
|
+
return execFileSync('git', ['-C', repositoryRoot, ...args], {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
10
|
+
}).replace(/\r?\n$/, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPrimaryWorktree(candidate: string, expectedCommonDirectory: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const root = fs.realpathSync.native(candidate);
|
|
16
|
+
if (!fs.statSync(root).isDirectory()) return false;
|
|
17
|
+
if (gitText(root, ['rev-parse', '--show-prefix']) !== '') return false;
|
|
18
|
+
|
|
19
|
+
const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
20
|
+
const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
|
|
21
|
+
return sameFilesystemEntry(commonDirectory, expectedCommonDirectory) &&
|
|
22
|
+
sameFilesystemEntry(gitDirectory, expectedCommonDirectory);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function worktreePaths(repositoryRoot: string): string[] {
|
|
29
|
+
const output = execFileSync('git', ['-C', repositoryRoot, 'worktree', 'list', '--porcelain', '-z'], {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return output
|
|
35
|
+
.split('\0\0')
|
|
36
|
+
.flatMap((record) => record.split('\0'))
|
|
37
|
+
.filter((field) => field.startsWith('worktree '))
|
|
38
|
+
.map((field) => field.slice('worktree '.length));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolvePrimaryWorktreeRoot(repositoryRoot: string): string {
|
|
42
|
+
try {
|
|
43
|
+
const root = fs.realpathSync.native(repositoryRoot);
|
|
44
|
+
if (!fs.statSync(root).isDirectory() || gitText(root, ['rev-parse', '--show-prefix']) !== '') {
|
|
45
|
+
throw new Error('repository root is not a worktree root');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
49
|
+
const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
|
|
50
|
+
if (sameFilesystemEntry(gitDirectory, commonDirectory)) return root;
|
|
51
|
+
|
|
52
|
+
const matches = worktreePaths(root).filter((candidate) => isPrimaryWorktree(candidate, commonDirectory));
|
|
53
|
+
if (matches.length !== 1) {
|
|
54
|
+
throw new Error(matches.length === 0 ? 'primary worktree metadata was not found' : 'primary worktree metadata is ambiguous');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return fs.realpathSync.native(matches[0]!);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
60
|
+
throw new Error(`server: unable to locate primary worktree: ${detail}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1140,9 +1140,33 @@ async function createExternalPullRequest(
|
|
|
1140
1140
|
}
|
|
1141
1141
|
if (base.prIdentity) {
|
|
1142
1142
|
const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1143
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked') {
|
|
1144
|
+
return withCreation(inspected, PRECONDITION_NOT_CREATED);
|
|
1145
|
+
}
|
|
1146
|
+
const deliveredHeadSha = typeof base.frontmatter.delivery_remote_head === 'string'
|
|
1147
|
+
? base.frontmatter.delivery_remote_head
|
|
1148
|
+
: undefined;
|
|
1149
|
+
const expectedHeadSha = verifiedHeadSha ?? deliveredHeadSha;
|
|
1150
|
+
if (!expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1151
|
+
platform: base.context.platform,
|
|
1152
|
+
capabilities: base.context.capabilities,
|
|
1153
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
1154
|
+
pullRequest: inspected.pullRequest,
|
|
1155
|
+
error: { code: 'PR_HEAD_UNVERIFIED', message: 'Unable to verify the bound pull request head SHA', retryable: false }
|
|
1156
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1157
|
+
const identity = validateWriterIdentity(base, inspected.pullRequest ?? null, {
|
|
1158
|
+
expectedHead: options.head,
|
|
1159
|
+
expectedHeadSha,
|
|
1160
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
1161
|
+
});
|
|
1162
|
+
if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1163
|
+
platform: base.context.platform,
|
|
1164
|
+
capabilities: base.context.capabilities,
|
|
1165
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
1166
|
+
pullRequest: inspected.pullRequest,
|
|
1167
|
+
error: identity.error
|
|
1168
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1169
|
+
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
1146
1170
|
}
|
|
1147
1171
|
if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
1148
1172
|
platform: base.context.platform,
|
|
@@ -272,12 +272,15 @@ function taskCreateDomainEvidence(
|
|
|
272
272
|
? output.task as Record<string, unknown> : null;
|
|
273
273
|
const taskId = typeof task?.id === 'string' ? task.id : null;
|
|
274
274
|
const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
|
|
275
|
-
|
|
275
|
+
const state = task?.state;
|
|
276
|
+
if (!taskId || !['active', 'blocked', 'completed', 'archive'].includes(String(state))) return { consistent: false };
|
|
277
|
+
if (state === 'active' ? !shortId : output.status !== 'no-op' || shortId !== null) return { consistent: false };
|
|
276
278
|
try {
|
|
277
279
|
const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
|
|
278
280
|
const shortIds = loadShortIdByTaskId(manifest.repoRoot);
|
|
279
281
|
return {
|
|
280
|
-
consistent: resolved.ok && resolved.taskId === taskId && resolved.state ===
|
|
282
|
+
consistent: resolved.ok && resolved.taskId === taskId && resolved.state === state
|
|
283
|
+
&& (state === 'active' ? shortIds.get(taskId) === shortId : !shortIds.has(taskId)),
|
|
281
284
|
taskId,
|
|
282
285
|
shortId
|
|
283
286
|
};
|
package/lib/server/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { resolvePrimaryWorktreeRoot } from '../git/primary-worktree.ts';
|
|
6
6
|
|
|
7
7
|
export type ServerLogConfig = {
|
|
8
8
|
path: string;
|
|
@@ -52,17 +52,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
52
52
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
// directory, keyed by the .airc.json "project" AND a stable hash of the repo
|
|
57
|
-
// root path:
|
|
58
|
-
// ~/.agent-infra/logs/<project>/<repo-hash>/server.log
|
|
59
|
-
// ~/.agent-infra/run/<project>/<repo-hash>/server.pid
|
|
60
|
-
// The <project> segment groups a project's checkouts for readability; the
|
|
61
|
-
// <repo-hash> segment guarantees that two checkouts/worktrees of the same
|
|
62
|
-
// project (same "project" but different absolute path) get ISOLATED runtime
|
|
63
|
-
// dirs, so they never read/control each other's daemon. Using os.homedir() +
|
|
64
|
-
// path.join keeps this correct on Windows too (C:\Users\<name>\.agent-infra\...).
|
|
65
|
-
// An explicit log.path in server.json/.local/env still overrides the log default.
|
|
55
|
+
// Runtime state is shared by all worktrees through the primary checkout.
|
|
66
56
|
function resolveProjectKey(repoRoot: string): string {
|
|
67
57
|
try {
|
|
68
58
|
const airc = JSON.parse(
|
|
@@ -77,13 +67,8 @@ function resolveProjectKey(repoRoot: string): string {
|
|
|
77
67
|
return path.basename(repoRoot);
|
|
78
68
|
}
|
|
79
69
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return createHash('sha256').update(repoRoot).digest('hex').slice(0, 12);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function runtimePath(repoRoot: string, projectKey: string, kind: 'logs' | 'run', file: string): string {
|
|
86
|
-
return path.join(homedir(), '.agent-infra', kind, projectKey, repoKey(repoRoot), file);
|
|
70
|
+
function runtimePath(projectKey: string, kind: 'logs' | 'run', file: string): string {
|
|
71
|
+
return path.join(homedir(), '.agent-infra', kind, projectKey, file);
|
|
87
72
|
}
|
|
88
73
|
|
|
89
74
|
function detectRepoRoot(): string {
|
|
@@ -185,7 +170,7 @@ export function validateServerConfig(committed: Record<string, unknown>): Server
|
|
|
185
170
|
}
|
|
186
171
|
|
|
187
172
|
export function loadServerConfig({ rootDir }: { rootDir?: string } = {}): ServerConfig {
|
|
188
|
-
const repoRoot = rootDir ?? detectRepoRoot();
|
|
173
|
+
const repoRoot = resolvePrimaryWorktreeRoot(rootDir ?? detectRepoRoot());
|
|
189
174
|
const agentsDir = path.join(repoRoot, '.agents');
|
|
190
175
|
|
|
191
176
|
const committed = readJsonIfPresent(path.join(agentsDir, 'server.json'));
|
|
@@ -205,18 +190,11 @@ export function loadServerConfig({ rootDir }: { rootDir?: string } = {}): Server
|
|
|
205
190
|
const projectKey = resolveProjectKey(repoRoot);
|
|
206
191
|
|
|
207
192
|
const log = isPlainObject(merged.log) ? merged.log : {};
|
|
208
|
-
// No explicit log.path → default under ~/.agent-infra/logs/<project>/.
|
|
209
|
-
// Explicit relative path resolves against the repo root; absolute is used as-is.
|
|
210
|
-
const explicitPath = typeof log.path === 'string' ? log.path : null;
|
|
211
|
-
const resolvedLogPath = explicitPath === null
|
|
212
|
-
? runtimePath(repoRoot, projectKey, 'logs', 'server.log')
|
|
213
|
-
: (path.isAbsolute(explicitPath) ? explicitPath : path.join(repoRoot, explicitPath));
|
|
214
|
-
|
|
215
193
|
return {
|
|
216
194
|
repoRoot,
|
|
217
|
-
pidFile: runtimePath(
|
|
195
|
+
pidFile: runtimePath(projectKey, 'run', 'server.pid'),
|
|
218
196
|
log: {
|
|
219
|
-
path:
|
|
197
|
+
path: runtimePath(projectKey, 'logs', 'server.log'),
|
|
220
198
|
rotateAtBytes: typeof log.rotateAtBytes === 'number' ? log.rotateAtBytes : DEFAULT_ROTATE_BYTES
|
|
221
199
|
},
|
|
222
200
|
heartbeatMs: typeof merged.heartbeatMs === 'number' ? merged.heartbeatMs : DEFAULT_SERVER_CONFIG.heartbeatMs,
|
package/lib/server/daemon.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function createMessageDispatcher(options: MessageDispatcherOptions): (mes
|
|
|
83
83
|
chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
|
|
84
84
|
throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
|
|
85
85
|
},
|
|
86
|
-
(emit) => runAiImpl(plan.argv, { onChunk: emit }),
|
|
86
|
+
(emit) => runAiImpl(plan.argv, { onChunk: emit, cwd: options.config.repoRoot }),
|
|
87
87
|
(outbound) => replyOutbound(message, outbound)
|
|
88
88
|
);
|
|
89
89
|
}
|
|
@@ -100,15 +100,17 @@ export function createMessageDispatcher(options: MessageDispatcherOptions): (mes
|
|
|
100
100
|
// - runDaemon() awaits a shutdown promise that only resolves once a
|
|
101
101
|
// SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
|
|
102
102
|
// the only keep-alive timer (that would let the process exit immediately).
|
|
103
|
-
export async function runDaemon(): Promise<void> {
|
|
103
|
+
export async function runDaemon({ rootDir }: { rootDir?: string } = {}): Promise<void> {
|
|
104
104
|
let config;
|
|
105
105
|
try {
|
|
106
|
-
config = loadServerConfig();
|
|
106
|
+
config = loadServerConfig({ rootDir });
|
|
107
107
|
} catch (error) {
|
|
108
108
|
process.stderr.write(`${errorMessage(error)}\n`);
|
|
109
109
|
process.exit(1);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
process.chdir(config.repoRoot);
|
|
113
|
+
|
|
112
114
|
const logger = createLogger(config.log);
|
|
113
115
|
logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
|
|
114
116
|
const ownStartTime = getProcessStartTime(process.pid);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { spawn, execFileSync } from 'node:child_process';
|
|
3
4
|
import { loadServerConfig } from './config.ts';
|
|
4
5
|
import type { ServerConfig } from './config.ts';
|
|
@@ -91,7 +92,7 @@ export async function start({ foreground = false }: StartOptions = {}): Promise<
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
if (foreground) {
|
|
94
|
-
await runDaemon();
|
|
95
|
+
await runDaemon({ rootDir: config.repoRoot });
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -103,8 +104,9 @@ export async function start({ foreground = false }: StartOptions = {}): Promise<
|
|
|
103
104
|
// Re-spawn ourselves detached. process.execArgv is forwarded so the dev path
|
|
104
105
|
// (node --experimental-strip-types ./bin/cli.ts) and the built path
|
|
105
106
|
// (node dist/bin/cli.js) both work.
|
|
106
|
-
const child = spawn(process.execPath, [...process.execArgv, cliEntry, 'server', '__daemon'], {
|
|
107
|
+
const child = spawn(process.execPath, [...process.execArgv, path.resolve(cliEntry), 'server', '__daemon'], {
|
|
107
108
|
detached: true,
|
|
109
|
+
cwd: config.repoRoot,
|
|
108
110
|
stdio: 'ignore'
|
|
109
111
|
});
|
|
110
112
|
child.unref();
|
package/lib/server/runner.ts
CHANGED
|
@@ -10,8 +10,9 @@ export type RunnerResult = {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export type RunnerOptions = {
|
|
13
|
-
spawn?: (file: string, args: string[], onChunk?: (chunk: string) => void | Promise<void
|
|
13
|
+
spawn?: (file: string, args: string[], onChunk?: (chunk: string) => void | Promise<void>, cwd?: string) => Promise<RunnerResult>;
|
|
14
14
|
onChunk?: (chunk: string) => void | Promise<void>;
|
|
15
|
+
cwd?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
function resolveCommand(file: string): string {
|
|
@@ -40,13 +41,15 @@ function needsShell(file: string): boolean {
|
|
|
40
41
|
function spawnCapture(
|
|
41
42
|
file: string,
|
|
42
43
|
args: string[],
|
|
43
|
-
onChunk?: (chunk: string) => void | Promise<void
|
|
44
|
+
onChunk?: (chunk: string) => void | Promise<void>,
|
|
45
|
+
cwd?: string
|
|
44
46
|
): Promise<RunnerResult> {
|
|
45
47
|
return new Promise((resolve, reject) => {
|
|
46
48
|
const resolvedFile = resolveCommand(file);
|
|
47
49
|
const child = spawn(resolvedFile, args, {
|
|
48
50
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
49
|
-
shell: needsShell(resolvedFile)
|
|
51
|
+
shell: needsShell(resolvedFile),
|
|
52
|
+
...(cwd === undefined ? {} : { cwd })
|
|
50
53
|
});
|
|
51
54
|
let stdout = '';
|
|
52
55
|
let stderr = '';
|
|
@@ -91,5 +94,5 @@ function spawnCapture(
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
export async function runAi(args: string[], options: RunnerOptions = {}): Promise<RunnerResult> {
|
|
94
|
-
return (options.spawn ?? spawnCapture)('ai', args, options.onChunk);
|
|
97
|
+
return (options.spawn ?? spawnCapture)('ai', args, options.onChunk, options.cwd);
|
|
95
98
|
}
|
|
@@ -13,10 +13,6 @@
|
|
|
13
13
|
"type": "object",
|
|
14
14
|
"additionalProperties": false,
|
|
15
15
|
"properties": {
|
|
16
|
-
"path": {
|
|
17
|
-
"type": "string",
|
|
18
|
-
"description": "Log file path. Defaults to ~/.agent-infra/logs/<project>/<repo-hash>/server.log (outside the repo; the repo-hash isolates checkouts that share a project). Relative paths resolve against the repo root; absolute paths are used as-is."
|
|
19
|
-
},
|
|
20
16
|
"rotateAtBytes": {
|
|
21
17
|
"type": "integer",
|
|
22
18
|
"minimum": 0,
|
|
@@ -9,6 +9,8 @@ import { parseImplementationInputs, selectPendingImplementationInput } from './i
|
|
|
9
9
|
import { parseVerdict } from './review-artifacts.ts';
|
|
10
10
|
import { extractSection, findSectionHeading } from './sections.ts';
|
|
11
11
|
import { receiptForOutput, sha256File } from './artifact-receipts.ts';
|
|
12
|
+
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
|
|
13
|
+
import { inspectReviewIdentity } from './review-identity.ts';
|
|
12
14
|
import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
|
|
13
15
|
import type { InvalidationDocument } from './invalidation.ts';
|
|
14
16
|
import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
|
|
@@ -228,7 +230,12 @@ function inspectArtifactDirectory(
|
|
|
228
230
|
if (!knownRounds.has(round)) diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
|
|
229
231
|
}
|
|
230
232
|
const latest = artifacts.at(-1) ?? null;
|
|
231
|
-
|
|
233
|
+
let hasReviewCodeReceipt = false;
|
|
234
|
+
if (family === 'review-code' && latest) {
|
|
235
|
+
try { hasReviewCodeReceipt = Boolean(receiptForOutput(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'), latest.name)); }
|
|
236
|
+
catch { hasReviewCodeReceipt = false; }
|
|
237
|
+
}
|
|
238
|
+
const reviewedInput = family.startsWith('review-') && latest && (family !== 'review-code' || hasReviewCodeReceipt)
|
|
232
239
|
? resolveReviewedInput(taskDir, latest, family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code', diagnostics)
|
|
233
240
|
: null;
|
|
234
241
|
return {
|
|
@@ -316,14 +323,16 @@ function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactEr
|
|
|
316
323
|
}
|
|
317
324
|
|
|
318
325
|
const REQUIRED_INPUT: Partial<Record<ArtifactFamily, ArtifactFamily>> = {
|
|
319
|
-
'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan'
|
|
326
|
+
'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan'
|
|
320
327
|
};
|
|
321
|
-
const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily,
|
|
322
|
-
analysis:
|
|
323
|
-
plan:
|
|
324
|
-
'review-
|
|
325
|
-
|
|
326
|
-
'
|
|
328
|
+
const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, readonly ArtifactFamily[]>> = {
|
|
329
|
+
analysis: ['review-analysis'],
|
|
330
|
+
plan: ['review-analysis', 'review-plan'],
|
|
331
|
+
'review-plan': ['review-analysis'],
|
|
332
|
+
code: ['review-plan'],
|
|
333
|
+
'review-code': ['code', 'plan', 'review-plan'],
|
|
334
|
+
'manual-validation': ['review-code'],
|
|
335
|
+
'validation-run': ['review-code']
|
|
327
336
|
};
|
|
328
337
|
function hasSelectionEvidence(
|
|
329
338
|
content: string,
|
|
@@ -336,6 +345,18 @@ function hasSelectionEvidence(
|
|
|
336
345
|
if (family.startsWith('review-')) {
|
|
337
346
|
const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
338
347
|
const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
|
|
348
|
+
if (family === 'review-code') {
|
|
349
|
+
if (!context.latest) return false;
|
|
350
|
+
try {
|
|
351
|
+
const reportContent = fs.readFileSync(context.latest.path, 'utf8');
|
|
352
|
+
const identity = inspectReviewIdentity(context.taskDir!, reportContent, options.repoRoot);
|
|
353
|
+
return identity.status !== 'matched'
|
|
354
|
+
|| !hasArtifactCompletionFact(content, context.latest.path, 'review-code.completed')
|
|
355
|
+
|| !hasArtifactCompletionLog(content, context.latest.name, 'review-code.completed');
|
|
356
|
+
} catch {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
339
360
|
if (currentInput && context.reviewedInput?.name !== currentInput.name) return true;
|
|
340
361
|
}
|
|
341
362
|
const target = family === 'analysis' ? 'analysis' : family === 'plan' ? 'plan' : family === 'code' ? 'code' : null;
|
|
@@ -450,9 +471,9 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
|
|
|
450
471
|
}
|
|
451
472
|
const optional = OPTIONAL_CONTEXT[inventory.family as ArtifactFamily];
|
|
452
473
|
if (optional) {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
inputs.push(context.latest);
|
|
474
|
+
for (const family of optional) {
|
|
475
|
+
const context = inspectTaskArtifacts(taskRef, family, options);
|
|
476
|
+
if (context.status === 'ready' && context.latest) inputs.push(context.latest);
|
|
456
477
|
}
|
|
457
478
|
}
|
|
458
479
|
if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
|
|
@@ -473,6 +494,9 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
|
|
|
473
494
|
const codeMax = latestCode?.round ?? 0;
|
|
474
495
|
const reviewMax = reviewCode.latest?.round ?? 0;
|
|
475
496
|
const inputs = [source.latest];
|
|
497
|
+
const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
|
|
498
|
+
const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
|
|
499
|
+
if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name) inputs.push(sourceReview.latest);
|
|
476
500
|
if (latestCode) {
|
|
477
501
|
try {
|
|
478
502
|
const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
|