@zhushanwen/pi-subagent-workflow 4.0.0 → 5.0.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/context-builder.md +1 -1
- package/agents/doc-reviewer.md +43 -0
- package/agents/explorer.md +1 -1
- package/agents/oracle.md +1 -1
- package/agents/orchestrator.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +1 -1
- package/package.json +3 -3
- package/skills/workflow-script-format/SKILL.md +42 -0
- package/src/execution/__tests__/agent-registry.test.ts +7 -7
- package/src/interface/__tests__/detectors.test.ts +28 -0
- package/src/interface/tool-workflow.ts +12 -5
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +817 -0
- package/src/orchestration/__tests__/script-lint.test.ts +318 -0
- package/src/orchestration/script-lint.ts +235 -15
- package/workflows/README.md +6 -3
- package/workflows/review-fix-loop-utils.cjs +840 -0
- package/workflows/review-fix-loop.js +584 -291
|
@@ -13,48 +13,75 @@
|
|
|
13
13
|
// ⚠️ 唯一带写操作的内置 workflow:fix 阶段会修改文件(autoCommit=true 时 commit)。
|
|
14
14
|
// ⚠️ lintScript 约束(本脚本已遵守):含 parallel() 入口,禁止 bare IIFE;
|
|
15
15
|
// agent() 调用顺序确定(批次按配置顺序稳定排序,callId 重放安全)。
|
|
16
|
+
//
|
|
17
|
+
// ⚠️ 与 main 的 4.0.0 版分叉(merge 时 add/add 冲突,刻意决策记录):
|
|
18
|
+
// 本版(feat-recursive-optimize)保留——纯函数拆到 review-fix-loop-utils.cjs(vitest 覆盖)、
|
|
19
|
+
// 支持 fallow-scan 前置批次、无默认批次(batch1..batchN/agents 必传)、recheckAfterFix 默认 false。
|
|
20
|
+
// main 4.0.0 版自包含 677 行、缺批次参数时默认单批 ["reviewer"]、recheckAfterFix 默认 false。
|
|
21
|
+
// merge 时保留本版(功能更全),详见 .changeset/tidy-waves-description-phase-lint.md。
|
|
16
22
|
|
|
17
23
|
const meta = {
|
|
18
24
|
name: "review-fix-loop",
|
|
19
|
-
description: "审查-修复循环:多批串行(批内并行 review → aggregate → fix → 重审直到 clean)。必填 targetType(git-diff/file/dir/text)+ target。批次由 batch1..batchN 控制(如 batch1=fallow-scan batch2=reviewer),用于前置检查先行的场景。注意:唯一带写操作/commit 副作用的内置 workflow,autoCommit 默认 false
|
|
20
|
-
phases: [
|
|
21
|
-
{ title: "Review", detail: "Batch: parallel review by configured agents" },
|
|
22
|
-
{ title: "Fix", detail: "Fix must-fix issues from aggregated report" },
|
|
23
|
-
],
|
|
25
|
+
description: "审查-修复循环:多批串行(批内并行 review → aggregate → fix → 重审直到 clean)。必填 targetType(git-diff/file/dir/text)+ target。批次由 batch1..batchN 控制(如 batch1=fallow-scan batch2=reviewer),用于前置检查先行的场景。注意:唯一带写操作/commit 副作用的内置 workflow,autoCommit 默认 false;skipCleanAgents 默认 true + recheckAfterFix 默认 false(clean agent 下轮跳过,与字面语义一致);传 recheckAfterFix=true 启用可选强回归模式(fix 后重派全批,clean agent 走限定 prompt 只审改动文件)。可选 fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds 控制修复 agent 与收敛终止(详见 workflows/README.md)。",
|
|
26
|
+
phases: ["Review", "Fix"],
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
// ── 参数解析 + 白名单校验(fail-fast) ────────────────────────────
|
|
27
30
|
|
|
28
|
-
const TARGET_TYPES = ["git-diff", "file", "dir", "text"];
|
|
29
|
-
const VALID_ARG_KEYS = new Set([
|
|
30
|
-
"targetType", "target", "agents", "batchNames", "reviewPrompt", "fixPrompt",
|
|
31
|
-
"autoCommit", "maxRounds", "stuckThreshold", "model", "skipCleanAgents",
|
|
32
|
-
"recheckAfterFix", "_runId",
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
31
|
function fail(msg) {
|
|
36
32
|
throw new Error("review-fix-loop: " + msg);
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
// ── 可测纯函数模块 ────────────────────────────────────────────────
|
|
36
|
+
// 参数校验(normalizeBool/normalizeInt/白名单)/批次解析/聚合结果解析/审查指令构建
|
|
37
|
+
// 的纯函数在 review-fix-loop-utils.cjs(与 recursive-split-utils.cjs 同款模式,
|
|
38
|
+
// vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts)。
|
|
39
|
+
// worker 运行时经 workerData.scriptPath 定位自身目录——内置 workflow 在 npm 包内,
|
|
40
|
+
// process.cwd() 是用户项目目录,不能作为锚点;其他引擎无 workerData 时回退 cwd。
|
|
41
|
+
const {
|
|
42
|
+
TARGET_TYPES,
|
|
43
|
+
VALID_ARG_KEYS,
|
|
44
|
+
normalizeBool,
|
|
45
|
+
normalizeInt,
|
|
46
|
+
parseBatches,
|
|
47
|
+
resolveBatchNames,
|
|
48
|
+
validateFallowScan,
|
|
49
|
+
buildReviewInstruction,
|
|
50
|
+
lockReviewBase,
|
|
51
|
+
buildScopedRecheckPrompt,
|
|
52
|
+
wrapUntrusted,
|
|
53
|
+
buildFixPrompt,
|
|
54
|
+
buildR2ReviewPrompt,
|
|
55
|
+
buildAggregatorPrompt,
|
|
56
|
+
resolveReviewReportPath,
|
|
57
|
+
normalizeFixResult,
|
|
58
|
+
validateFixResult,
|
|
59
|
+
reconcileIssues,
|
|
60
|
+
normalizeReviewResult,
|
|
61
|
+
computeKnownRemaining,
|
|
62
|
+
checkConvergence,
|
|
63
|
+
findNeedsRedesign,
|
|
64
|
+
parseResult,
|
|
65
|
+
normalizeAggregatorResult,
|
|
66
|
+
parseAggregatedMd,
|
|
67
|
+
shouldRetryWithReviewPrefix,
|
|
68
|
+
resolveAgentDefs,
|
|
69
|
+
recordAgentClean,
|
|
70
|
+
recordAgentDirty,
|
|
71
|
+
shouldSkipAgent,
|
|
72
|
+
updateStuckState,
|
|
73
|
+
resolveBatchTerminated,
|
|
74
|
+
} = require(
|
|
75
|
+
(typeof workerData !== "undefined" && workerData && typeof workerData.scriptPath === "string"
|
|
76
|
+
? require("path").dirname(workerData.scriptPath)
|
|
77
|
+
: process.cwd()) + "/review-fix-loop-utils.cjs"
|
|
78
|
+
);
|
|
52
79
|
|
|
53
80
|
// 白名单校验:未知参数名(防 batchX 拼错如 batchl)→ 报错
|
|
54
81
|
for (const key of Object.keys($ARGS)) {
|
|
55
82
|
if (VALID_ARG_KEYS.has(key)) continue;
|
|
56
83
|
if (/^batch\d+$/.test(key)) continue;
|
|
57
|
-
fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/model/skipCleanAgents/recheckAfterFix)");
|
|
84
|
+
fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/model/skipCleanAgents/recheckAfterFix/fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds)");
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
const targetType = $ARGS.targetType;
|
|
@@ -70,77 +97,52 @@ const reviewPrompt = typeof $ARGS.reviewPrompt === "string" && $ARGS.reviewPromp
|
|
|
70
97
|
const fixPrompt = typeof $ARGS.fixPrompt === "string" && $ARGS.fixPrompt.trim()
|
|
71
98
|
? $ARGS.fixPrompt.trim()
|
|
72
99
|
: "修复全部 must-fix 问题(critical/major)。最小正确修复,不做重构、不做风格改动。";
|
|
73
|
-
const autoCommit = normalizeBool($ARGS.autoCommit, "autoCommit", false);
|
|
74
|
-
const maxRounds = normalizeInt($ARGS.maxRounds, "maxRounds", 10);
|
|
75
|
-
const stuckThreshold = normalizeInt($ARGS.stuckThreshold, "stuckThreshold", 3);
|
|
76
|
-
const skipCleanAgents = normalizeBool($ARGS.skipCleanAgents, "skipCleanAgents", true);
|
|
77
|
-
|
|
100
|
+
const autoCommit = normalizeBool($ARGS.autoCommit, "autoCommit", false, fail);
|
|
101
|
+
const maxRounds = normalizeInt($ARGS.maxRounds, "maxRounds", 10, fail);
|
|
102
|
+
const stuckThreshold = normalizeInt($ARGS.stuckThreshold, "stuckThreshold", 3, fail);
|
|
103
|
+
const skipCleanAgents = normalizeBool($ARGS.skipCleanAgents, "skipCleanAgents", true, fail);
|
|
104
|
+
// 默认 recheckAfterFix=false:clean agent 下轮跳过(与 skipCleanAgents=true 字面语义一致),
|
|
105
|
+
// RC-5(fix 后全批全量重审放大 token)在默认场景消失。传 true 启用可选强回归模式:fix 后重派
|
|
106
|
+
// 全批,clean agent 走限定 prompt(buildScopedRecheckPrompt,只审 modifiedFiles,5.5)。
|
|
107
|
+
const recheckAfterFix = normalizeBool($ARGS.recheckAfterFix, "recheckAfterFix", false, fail);
|
|
108
|
+
// fixAgent(5.3):值语义同 batchN 的 agent 项(内置名 / agent.md 路径),解析复用
|
|
109
|
+
// resolveAgentDefs 白名单与加载逻辑。传入时 fix 阶段用 agent({agent: ...}) 派发(代码场景
|
|
110
|
+
// 的 verify 命令写在该 agent.md 内);未传保持现状(通用 subagent + 内联 prompt)。
|
|
111
|
+
const FIX_AGENT_RAW = typeof $ARGS.fixAgent === "string" && $ARGS.fixAgent.trim()
|
|
112
|
+
? $ARGS.fixAgent.trim() : undefined;
|
|
113
|
+
const FIX_DEF = FIX_AGENT_RAW ? resolveAgentDefs([FIX_AGENT_RAW])[0] : null;
|
|
114
|
+
// 5.7 收敛终止参数:maxFixAttempts(needs-redesign 阈值,RC-7)/ convergeNewIssues +
|
|
115
|
+
// convergeRounds(新发现率收敛阈值)
|
|
116
|
+
const maxFixAttempts = normalizeInt($ARGS.maxFixAttempts, "maxFixAttempts", 2, fail);
|
|
117
|
+
const convergeNewIssues = normalizeInt($ARGS.convergeNewIssues, "convergeNewIssues", 1, fail);
|
|
118
|
+
const convergeRounds = normalizeInt($ARGS.convergeRounds, "convergeRounds", 2, fail);
|
|
78
119
|
const MODEL = typeof $ARGS.model === "string" && $ARGS.model.trim() ? $ARGS.model.trim() : undefined;
|
|
79
120
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
case "file":
|
|
88
|
-
return "Read and review the file: " + target;
|
|
89
|
-
case "dir":
|
|
90
|
-
return "Explore and review the directory: " + target + " (list files, then read the relevant ones)";
|
|
91
|
-
case "text":
|
|
92
|
-
return "Review target: " + target;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const reviewInstruction = buildReviewInstruction();
|
|
96
|
-
|
|
97
|
-
// 批次解析:batch1..batchN(缺号报错)/ agents 简写 / 默认单批 [reviewer]
|
|
98
|
-
function parseBatches() {
|
|
99
|
-
const batchKeys = Object.keys($ARGS)
|
|
100
|
-
.filter((k) => /^batch\d+$/.test(k))
|
|
101
|
-
.sort((a, b) => parseInt(a.slice(5), 10) - parseInt(b.slice(5), 10));
|
|
102
|
-
const nums = batchKeys.map((k) => parseInt(k.slice(5), 10));
|
|
103
|
-
for (let i = 1; i <= nums.length; i++) {
|
|
104
|
-
if (!nums.includes(i)) fail("批次参数缺号:有 batch" + nums.join("/") + " 但无 batch" + i + "(批次必须连续编号)");
|
|
105
|
-
}
|
|
106
|
-
if ($ARGS.agents !== undefined && $ARGS.batch1 !== undefined) {
|
|
107
|
-
fail("agents 与 batch1 不能同时传(agents 是单批简写)");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let rawBatches;
|
|
111
|
-
if (batchKeys.length > 0) {
|
|
112
|
-
rawBatches = batchKeys.map((k) => $ARGS[k]);
|
|
113
|
-
} else if ($ARGS.agents !== undefined) {
|
|
114
|
-
rawBatches = [$ARGS.agents];
|
|
121
|
+
// base 锁定(RC-6,5.6):git-diff 场景 run 启动时锁定 base commit,全程用锁定 hash 构造
|
|
122
|
+
// diff 指令,防止 run 期间 base ref 被更新导致各轮 diff 范围不一致。rev-parse 失败(非 git
|
|
123
|
+
// 目录 / ref 不存在)降级用原 ref 并 warn,锁定结果随 state.meta.baseHash 记录。
|
|
124
|
+
const lockedBase = lockReviewBase(targetType, target);
|
|
125
|
+
if (targetType === "git-diff") {
|
|
126
|
+
if (lockedBase.hash) {
|
|
127
|
+
log("Locked review base: " + target + " -> " + lockedBase.hash);
|
|
115
128
|
} else {
|
|
116
|
-
|
|
129
|
+
log("WARN: git rev-parse " + target + " failed, falling back to ref for diff base: " + target);
|
|
117
130
|
}
|
|
118
|
-
|
|
119
|
-
return rawBatches.map((raw, idx) => {
|
|
120
|
-
if (typeof raw !== "string" || !raw.trim()) fail("batch" + (idx + 1) + " 不能为空");
|
|
121
|
-
const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
122
|
-
if (names.length === 0) fail("batch" + (idx + 1) + " 为空(逗号分隔 agent 名/文件路径)");
|
|
123
|
-
if (new Set(names).size !== names.length) fail("batch" + (idx + 1) + " 内存在重复 agent: " + names);
|
|
124
|
-
return names;
|
|
125
|
-
});
|
|
126
131
|
}
|
|
127
|
-
const
|
|
132
|
+
const reviewInstruction = buildReviewInstruction(targetType, lockedBase.base);
|
|
133
|
+
|
|
134
|
+
// 批次解析:batch1..batchN(缺号报错)/ agents 简写;无默认批次——缺批次参数时
|
|
135
|
+
// parseBatches 直接 fail-fast(与头注释「batch1..batchN/agents 必传」一致)
|
|
136
|
+
const BATCHES = parseBatches($ARGS, fail);
|
|
128
137
|
|
|
129
138
|
// batchNames(数量校验)
|
|
130
139
|
const rawBatchNames = typeof $ARGS.batchNames === "string" && $ARGS.batchNames.trim()
|
|
131
140
|
? $ARGS.batchNames.split(",").map((s) => s.trim()).filter(Boolean)
|
|
132
141
|
: [];
|
|
133
|
-
|
|
134
|
-
fail("batchNames 数量(" + rawBatchNames.length + ")必须与批数(" + BATCHES.length + ")一致");
|
|
135
|
-
}
|
|
136
|
-
const BATCH_NAMES = rawBatchNames.length ? rawBatchNames : BATCHES.map((_, i) => "batch-" + (i + 1));
|
|
142
|
+
const BATCH_NAMES = resolveBatchNames(rawBatchNames, BATCHES, fail);
|
|
137
143
|
|
|
138
144
|
// fallow-scan 只在 git-diff 类型下有意义
|
|
139
|
-
|
|
140
|
-
if (BATCHES[i].includes("fallow-scan") && targetType !== "git-diff") {
|
|
141
|
-
fail("fallow-scan 只支持 targetType=git-diff(它审查 git 变更的静态分析),实际 targetType=" + targetType);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
145
|
+
validateFallowScan(BATCHES, targetType, fail);
|
|
144
146
|
|
|
145
147
|
// ── Schemas ─────────────────────────────────────────────────────────
|
|
146
148
|
|
|
@@ -148,8 +150,22 @@ const reviewerSchema = {
|
|
|
148
150
|
type: "object",
|
|
149
151
|
properties: {
|
|
150
152
|
report_file: { type: "string", description: "Absolute path to the written review report (.md)" },
|
|
153
|
+
report_content: { type: "string", description: "Full markdown report body (for schema-only agents without write tool; workflow writes it to <roundDir>/<report>.md)" },
|
|
151
154
|
must_fix: { type: "number", description: "Number of must-fix (critical+major) issues found" },
|
|
152
155
|
suggestion: { type: "number", description: "Number of suggestion-level (minor) issues found" },
|
|
156
|
+
reconciliation: {
|
|
157
|
+
type: "array",
|
|
158
|
+
items: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
prev_id: { type: "string", description: "Issue id from the previous round (reconciliation continuation)" },
|
|
162
|
+
status: { type: "string", description: "fixed / not-fixed / regressed / escalate — only fixed counts as resolved; fix result claiming fixed is NOT evidence; escalate = a DEFERRED issue whose context was changed by this round's fix (workflow re-opens it for fixing)" },
|
|
163
|
+
evidence: { type: "string", description: "What was read/confirmed (file + what changed)" },
|
|
164
|
+
},
|
|
165
|
+
required: ["prev_id", "status"],
|
|
166
|
+
},
|
|
167
|
+
description: "R2+ reconciliation table (structured): MANDATORY for R2+ rounds, optional for R1",
|
|
168
|
+
},
|
|
153
169
|
},
|
|
154
170
|
required: ["report_file", "must_fix", "suggestion"],
|
|
155
171
|
};
|
|
@@ -160,10 +176,69 @@ const aggregatorSchema = {
|
|
|
160
176
|
report_file: { type: "string", description: "Absolute path to aggregated.md" },
|
|
161
177
|
must_fix: { type: "number", description: "Total must-fix after dedup across all dimensions" },
|
|
162
178
|
suggestion: { type: "number", description: "Total suggestions after dedup across all dimensions" },
|
|
179
|
+
must_fix_ids: {
|
|
180
|
+
type: "array",
|
|
181
|
+
// M1: 支持 [{id, severity}] 对象(severity: critical/major/minor——converged 终止的
|
|
182
|
+
// 「无 critical」判定数据源)+ 旧格式 string[] 兼容。ajv 权威校验两者皆放行。
|
|
183
|
+
items: {
|
|
184
|
+
oneOf: [
|
|
185
|
+
{ type: "string" },
|
|
186
|
+
{
|
|
187
|
+
type: "object",
|
|
188
|
+
required: ["id"],
|
|
189
|
+
properties: {
|
|
190
|
+
id: { type: "string" },
|
|
191
|
+
severity: { type: "string" },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
description: "Issue ids of the deduplicated must-fix list (MF-1..N), matching the first column of the markdown table",
|
|
197
|
+
},
|
|
198
|
+
fixes_caution: {
|
|
199
|
+
type: "array",
|
|
200
|
+
items: { type: "string" },
|
|
201
|
+
description: "Caution entries for weak-evidence or high-risk claims (passed to fix stage)",
|
|
202
|
+
},
|
|
163
203
|
},
|
|
164
204
|
required: ["report_file", "must_fix", "suggestion"],
|
|
165
205
|
};
|
|
166
206
|
|
|
207
|
+
// fix schema(5.3):object[](issue_id/description/self_check/affected_files)+ deferred。
|
|
208
|
+
// 兼容性:normalizeFixResult 对旧格式(fixes string[]、无 deferred)兜底解析。
|
|
209
|
+
const fixSchema = {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
fixed_count: { type: "number", description: "Number of issues fixed" },
|
|
213
|
+
fixes: {
|
|
214
|
+
type: "array",
|
|
215
|
+
items: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
issue_id: { type: "string", description: "Issue identifier from the aggregated report" },
|
|
219
|
+
description: { type: "string", description: "One-line description of the fix" },
|
|
220
|
+
self_check: { type: "string", description: "grep command + hit count + sync action proving the fix is complete" },
|
|
221
|
+
affected_files: { type: "array", items: { type: "string" }, description: "Files touched by this fix + files checked/synced as reference points" },
|
|
222
|
+
},
|
|
223
|
+
// m3: issue_id 是 ES3 交叉校验(must-fix 必须全进 fixes[])的匹配键,必填
|
|
224
|
+
required: ["issue_id"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
deferred: {
|
|
228
|
+
type: "array",
|
|
229
|
+
items: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
issue_id: { type: "string" },
|
|
233
|
+
severity: { type: "string", description: "Must be minor — critical/major must not be deferred" },
|
|
234
|
+
reason: { type: "string", description: "Concrete cost description: which files/mechanisms involved, why high cost, suggested follow-up" },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
required: ["fixed_count"],
|
|
240
|
+
};
|
|
241
|
+
|
|
167
242
|
// ── Per-run isolation: runId-scoped directories ─────────────────────
|
|
168
243
|
|
|
169
244
|
const fs = require("fs");
|
|
@@ -191,6 +266,11 @@ function loadState() {
|
|
|
191
266
|
agentStatus: {},
|
|
192
267
|
fixCount: 0,
|
|
193
268
|
batches: [],
|
|
269
|
+
fixResults: [],
|
|
270
|
+
issues: undefined,
|
|
271
|
+
knownRemaining: [],
|
|
272
|
+
convergeStreak: 0,
|
|
273
|
+
lastModifiedFiles: [],
|
|
194
274
|
};
|
|
195
275
|
}
|
|
196
276
|
}
|
|
@@ -201,112 +281,29 @@ function saveState(state) {
|
|
|
201
281
|
fs.renameSync(tmp, STATE_FILE);
|
|
202
282
|
}
|
|
203
283
|
|
|
204
|
-
// clean
|
|
205
|
-
|
|
206
|
-
const s = state.agentStatus[agentName] || { lastCleanBatch: 0, lastCleanFixCount: 0, lastActiveRound: 0, lastMustFix: undefined };
|
|
207
|
-
s.lastCleanBatch = batchIndex;
|
|
208
|
-
s.lastCleanFixCount = state.fixCount;
|
|
209
|
-
s.lastActiveRound = batchIndex;
|
|
210
|
-
state.agentStatus[agentName] = s;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function recordAgentDirty(state, agentName, mustFix, batchIndex) {
|
|
214
|
-
const s = state.agentStatus[agentName] || { lastCleanBatch: 0, lastCleanFixCount: 0, lastActiveRound: 0, lastMustFix: undefined };
|
|
215
|
-
s.lastActiveRound = batchIndex;
|
|
216
|
-
s.lastMustFix = mustFix;
|
|
217
|
-
state.agentStatus[agentName] = s;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
221
|
-
|
|
222
|
-
function parseResult(raw) {
|
|
223
|
-
if (typeof raw === "object" && raw !== null) return raw;
|
|
224
|
-
if (typeof raw === "string") {
|
|
225
|
-
let s = raw.trim();
|
|
226
|
-
const fence = s.match(/^```(?:json)?\s*\n([\s\S]*?)\n?```\s*$/i);
|
|
227
|
-
if (fence) s = fence[1].trim();
|
|
228
|
-
if (!s.startsWith("{") && !s.startsWith("[")) {
|
|
229
|
-
const first = s.indexOf("{");
|
|
230
|
-
const last = s.lastIndexOf("}");
|
|
231
|
-
if (first !== -1 && last > first) s = s.slice(first, last + 1);
|
|
232
|
-
}
|
|
233
|
-
try { return JSON.parse(s); } catch { /* fall through */ }
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
284
|
+
// clean 记录(recordAgentClean/recordAgentDirty 与跨批跳过判定 shouldSkipAgent
|
|
285
|
+
// 在 review-fix-loop-utils.cjs,vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts)
|
|
237
286
|
|
|
238
|
-
|
|
239
|
-
const parsed = parseResult(raw);
|
|
240
|
-
if (!parsed) return null;
|
|
241
|
-
const mustFix =
|
|
242
|
-
typeof parsed.must_fix === "number" ? parsed.must_fix :
|
|
243
|
-
typeof parsed.totalMustFix === "number" ? parsed.totalMustFix :
|
|
244
|
-
typeof parsed.mustFix === "number" ? parsed.mustFix : undefined;
|
|
245
|
-
const suggestion =
|
|
246
|
-
typeof parsed.suggestion === "number" ? parsed.suggestion :
|
|
247
|
-
typeof parsed.totalSuggestions === "number" ? parsed.totalSuggestions :
|
|
248
|
-
typeof parsed.suggestions === "number" ? parsed.suggestions : 0;
|
|
249
|
-
if (typeof mustFix !== "number") return null;
|
|
250
|
-
return { report_file: parsed.report_file || parsed.reportFile, must_fix: mustFix, suggestion };
|
|
251
|
-
}
|
|
287
|
+
// ── Agent defs(loadAgentMd/resolveAgentDefs 在 review-fix-loop-utils.cjs) ──
|
|
252
288
|
|
|
253
|
-
|
|
254
|
-
const mustFixMatch = content.match(/[-*]\s*Must[-_]fix\s*[::]\s*(\d+)/i);
|
|
255
|
-
if (!mustFixMatch) return null;
|
|
256
|
-
const suggestionMatch = content.match(/[-*]\s*Suggestions?\s*[::]\s*(\d+)/i);
|
|
257
|
-
return {
|
|
258
|
-
must_fix: parseInt(mustFixMatch[1], 10),
|
|
259
|
-
suggestion: suggestionMatch ? parseInt(suggestionMatch[1], 10) : 0,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
289
|
+
// ── Build review calls ──────────────────────────────────────────────
|
|
262
290
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
291
|
+
// 上一轮 fix 的 modifiedFiles(git diff 实测,fix 阶段写入 batchRounds)。
|
|
292
|
+
// recheck 限定 prompt(scoped)的 scope 来源(初版 = modifiedFiles)。
|
|
293
|
+
function lastModifiedFiles() {
|
|
294
|
+
// M4: 批内轮次循环中 state.batches 尚未 push(仅在终止/批结束 push)——scoped 分支
|
|
295
|
+
// (recheckAfterFix=true 的下轮 review)执行时读不到本批数据。fix 阶段把
|
|
296
|
+
// modifiedFiles 同步写入 state.lastModifiedFiles(即时字段,批内立即可用),
|
|
297
|
+
// 此处优先读它;fallback 旧路径(跨批场景)。
|
|
298
|
+
if (state.lastModifiedFiles && Array.isArray(state.lastModifiedFiles)) {
|
|
299
|
+
return state.lastModifiedFiles;
|
|
270
300
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (content.startsWith("---")) {
|
|
275
|
-
const closeIdx = content.indexOf("---", 3);
|
|
276
|
-
if (closeIdx !== -1) {
|
|
277
|
-
const yaml = content.slice(3, closeIdx);
|
|
278
|
-
body = content.slice(closeIdx + 3).trim();
|
|
279
|
-
const extract = (key) => {
|
|
280
|
-
const m = yaml.match(new RegExp("^" + key + ":\\s*(.+)$", "m"));
|
|
281
|
-
if (!m) return undefined;
|
|
282
|
-
let v = m[1].trim();
|
|
283
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
284
|
-
return v || undefined;
|
|
285
|
-
};
|
|
286
|
-
name = extract("name") || name;
|
|
287
|
-
model = extract("model");
|
|
288
|
-
description = extract("description");
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return { name, model, description, systemPrompt: body, report: name, title: description || name, isCustom: true };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ── Agent defs 解析(每批元素统一解析) ─────────────────────────────
|
|
295
|
-
|
|
296
|
-
// fallow-scan:内置工具型 def(无 .md,跑 fallow audit 静态分析)
|
|
297
|
-
const FALLOW_DEF = { name: "fallow-scan", title: "FALLOW STATIC ANALYSIS", report: "fallow-scan", isFallow: true };
|
|
298
|
-
|
|
299
|
-
function resolveAgentDefs(batchNames) {
|
|
300
|
-
return batchNames.map((item) => {
|
|
301
|
-
if (item === "fallow-scan") return FALLOW_DEF;
|
|
302
|
-
if (item.includes("/") || item.endsWith(".md")) return loadAgentMd(item);
|
|
303
|
-
return { name: item, report: item.replace(/^review-/, ""), title: item.toUpperCase() };
|
|
304
|
-
});
|
|
301
|
+
const b = state.batches[state.batches.length - 1];
|
|
302
|
+
const r = b && b.rounds && b.rounds[b.rounds.length - 1];
|
|
303
|
+
return (r && Array.isArray(r.modifiedFiles)) ? r.modifiedFiles : [];
|
|
305
304
|
}
|
|
306
305
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
function buildReviewCall(def, round, max, batchIndex, roundDir) {
|
|
306
|
+
function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
|
|
310
307
|
const header = "Batch " + batchIndex + " Round " + round + "/" + max + " — " + BATCH_NAMES[batchIndex - 1];
|
|
311
308
|
const prevBatchesHint = batchIndex > 1
|
|
312
309
|
? "\nPrior batch reports (optional context): " + RUN_ROOT + "/batch-*/ (use read)"
|
|
@@ -316,6 +313,10 @@ function buildReviewCall(def, round, max, batchIndex, roundDir) {
|
|
|
316
313
|
schema: reviewerSchema,
|
|
317
314
|
description: def.name,
|
|
318
315
|
timeoutMs: 1_800_000,
|
|
316
|
+
// returnMeta: true — 与 recursive-split 脚本的 executeActionAgent 对齐:失败时 resolve
|
|
317
|
+
// {value, error},raw.error 可检测(review- 前缀兜底/结构化终止可达);成功时
|
|
318
|
+
// value = parsedOutput ?? content,parseResult 作用于 raw.value(MF-1)。
|
|
319
|
+
returnMeta: true,
|
|
319
320
|
};
|
|
320
321
|
|
|
321
322
|
if (def.isFallow) {
|
|
@@ -329,7 +330,7 @@ function buildReviewCall(def, round, max, batchIndex, roundDir) {
|
|
|
329
330
|
"Steps:",
|
|
330
331
|
"1. Check if fallow is installed: `which fallow`",
|
|
331
332
|
"2. If NOT installed: write the report with a one-line note, must_fix=0, suggestion=0.",
|
|
332
|
-
"3. If installed, run: `fallow audit --base " +
|
|
333
|
+
"3. If installed, run: `fallow audit --base " + lockedBase.base + " --format json --quiet`",
|
|
333
334
|
"4. Extract: complexity hotspots, dead code, unused exports, circular deps",
|
|
334
335
|
"5. Classify findings: critical/major count into must_fix; minor into suggestion.",
|
|
335
336
|
"",
|
|
@@ -339,6 +340,50 @@ function buildReviewCall(def, round, max, batchIndex, roundDir) {
|
|
|
339
340
|
};
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
// R2+ 三段式分支(5.2):round>1 且非 scoped → verify-first 对账 + known-remaining + 收敛 hunt。
|
|
344
|
+
// scoped 分支(recheck 限定)在下方单独处理(含对账段)。
|
|
345
|
+
if (round > 1 && !scoped) {
|
|
346
|
+
const prevRoundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1);
|
|
347
|
+
const r2Spec = def.isCustom
|
|
348
|
+
? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt
|
|
349
|
+
: "";
|
|
350
|
+
return {
|
|
351
|
+
...base,
|
|
352
|
+
schema: { ...reviewerSchema, required: [...reviewerSchema.required, "reconciliation"] },
|
|
353
|
+
prompt: buildR2ReviewPrompt({
|
|
354
|
+
header, round, max, roundDir,
|
|
355
|
+
reportFile: def.report,
|
|
356
|
+
aggPath: prevRoundDir + "/aggregated.md",
|
|
357
|
+
fixResult: state.fixResults && state.fixResults.length
|
|
358
|
+
? state.fixResults[state.fixResults.length - 1]
|
|
359
|
+
: null,
|
|
360
|
+
knownRemaining: (state.knownRemaining && Array.isArray(state.knownRemaining)) ? state.knownRemaining : [],
|
|
361
|
+
}) + r2Spec,
|
|
362
|
+
agent: def.isCustom ? undefined : def.name,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// recheck 限定分支(5.5):clean agent 重派时只审 fix 改动文件 + 自检关联点
|
|
367
|
+
// (scope = modifiedFiles ∪ affected_files,后者来自 state.fixImpactFiles),不诱导全量重扫。
|
|
368
|
+
if (scoped) {
|
|
369
|
+
return {
|
|
370
|
+
...base,
|
|
371
|
+
// m2: scoped 分支与 R2+ 分支一致——reconciliation 必填(recheck 也须对账前轮 fix)
|
|
372
|
+
schema: { ...reviewerSchema, required: [...reviewerSchema.required, "reconciliation"] },
|
|
373
|
+
prompt: buildScopedRecheckPrompt({
|
|
374
|
+
header, round, max, roundDir,
|
|
375
|
+
reportFile: def.report,
|
|
376
|
+
modifiedFiles: lastModifiedFiles(),
|
|
377
|
+
affectedFiles: (state.fixImpactFiles && Array.isArray(state.fixImpactFiles)) ? state.fixImpactFiles : [],
|
|
378
|
+
aggPath: RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1) + "/aggregated.md",
|
|
379
|
+
fixResult: state.fixResults && state.fixResults.length
|
|
380
|
+
? state.fixResults[state.fixResults.length - 1]
|
|
381
|
+
: null,
|
|
382
|
+
}) + (def.isCustom ? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt : ""),
|
|
383
|
+
agent: def.isCustom ? undefined : def.name,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
342
387
|
const spec = def.isCustom
|
|
343
388
|
? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt
|
|
344
389
|
: "";
|
|
@@ -359,11 +404,11 @@ function buildReviewCall(def, round, max, batchIndex, roundDir) {
|
|
|
359
404
|
};
|
|
360
405
|
}
|
|
361
406
|
|
|
362
|
-
// agent 名解析失败(AgentRegistry not found
|
|
407
|
+
// agent 名解析失败(AgentRegistry not found,报错文案 `Agent "${name}" not found.`)时,尝试 review- 前缀兜底
|
|
363
408
|
async function runReviewAgent(call) {
|
|
364
409
|
let raw = await agent(call);
|
|
365
|
-
if (raw && typeof raw === "object" && raw.error
|
|
366
|
-
&& raw.error
|
|
410
|
+
if (raw && typeof raw === "object" && raw.error
|
|
411
|
+
&& shouldRetryWithReviewPrefix(raw.error, call.agent)) {
|
|
367
412
|
log("Agent not found: " + call.agent + " — retrying with review- prefix");
|
|
368
413
|
raw = await agent({ ...call, agent: "review-" + call.agent });
|
|
369
414
|
}
|
|
@@ -373,6 +418,9 @@ async function runReviewAgent(call) {
|
|
|
373
418
|
// ── Main loop: batches (serial) × rounds (per-batch) ────────────────
|
|
374
419
|
|
|
375
420
|
const state = loadState();
|
|
421
|
+
// 5.6 锁定结果落 state.meta.baseHash(commit 1 声称与实现一致化,run 后可从 state.json 追溯审查基线)
|
|
422
|
+
state.meta = state.meta || {};
|
|
423
|
+
state.meta.baseHash = lockedBase.hash || "";
|
|
376
424
|
let totalFixed = 0;
|
|
377
425
|
let terminated = "clean";
|
|
378
426
|
let finalMessage = "";
|
|
@@ -381,17 +429,24 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
381
429
|
const defs = resolveAgentDefs(BATCHES[batchIndex - 1]);
|
|
382
430
|
const cleanNames = new Set();
|
|
383
431
|
let round = 0;
|
|
384
|
-
let
|
|
432
|
+
let prevMustFix = -1;
|
|
385
433
|
let stuckCount = 0;
|
|
386
434
|
let batchClean = false;
|
|
387
435
|
let roundHasFix = false; // recheckAfterFix 用:上轮是否有 fix
|
|
388
436
|
const batchRounds = [];
|
|
389
437
|
|
|
438
|
+
// MF-1 批级 review 状态隔离:每批开始重置 issue 追踪 / 收敛计数 / known-remaining,
|
|
439
|
+
// 防止跨批 newFindings 语义污染(firstSeen 用批内 round 号写入、convergeStreak 跨批
|
|
440
|
+
// 累加会让前批收敛状态泄漏到后批,复合导致 converged 错误跳过后续批次)。
|
|
441
|
+
state.issues = undefined;
|
|
442
|
+
state.convergeStreak = 0;
|
|
443
|
+
state.knownRemaining = [];
|
|
444
|
+
|
|
390
445
|
// 跨批跳过:agent 在更早批 clean 且此后无 fix → 本批不派发
|
|
391
446
|
if (batchIndex > 1) {
|
|
392
447
|
for (const def of defs) {
|
|
393
448
|
const s = state.agentStatus[def.name];
|
|
394
|
-
if (s
|
|
449
|
+
if (shouldSkipAgent(s, state.fixCount, batchIndex)) {
|
|
395
450
|
cleanNames.add(def.name);
|
|
396
451
|
log("Cross-batch skip: " + def.name + " (clean in batch " + s.lastCleanBatch + ", no fix since)");
|
|
397
452
|
}
|
|
@@ -407,8 +462,10 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
407
462
|
fs.mkdirSync(roundDir, { recursive: true });
|
|
408
463
|
|
|
409
464
|
let active = defs.filter((def) => !(skipCleanAgents && cleanNames.has(def.name)));
|
|
465
|
+
let scopedClean = new Set(); // recheckAfterFix 重派时:上一轮 clean 的 agent 本轮走限定 prompt
|
|
410
466
|
if (recheckAfterFix && round > 1 && roundHasFix) {
|
|
411
|
-
|
|
467
|
+
scopedClean = new Set(cleanNames); // 重派前快照上一轮 clean 集合
|
|
468
|
+
active = defs; // fix 后重派全批(强回归模式)
|
|
412
469
|
cleanNames.clear();
|
|
413
470
|
}
|
|
414
471
|
|
|
@@ -419,21 +476,47 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
419
476
|
}
|
|
420
477
|
|
|
421
478
|
log("Review: " + active.map((d) => d.name).join(", ") + " (" + active.length + " agent(s) in parallel)...");
|
|
422
|
-
const calls = active.map((def) => buildReviewCall(def, round, maxRounds, batchIndex, roundDir));
|
|
423
|
-
const allRaw = await parallel(calls);
|
|
479
|
+
const calls = active.map((def) => buildReviewCall(def, round, maxRounds, batchIndex, roundDir, scopedClean.has(def.name)));
|
|
480
|
+
const allRaw = await parallel(calls.map(runReviewAgent));
|
|
424
481
|
|
|
425
482
|
// per-agent 结果区分:parallel 结果与 calls 一一对应
|
|
426
483
|
const reviewResults = [];
|
|
427
484
|
const agentRoundResults = [];
|
|
485
|
+
const reconSeen = new Set(); // R2+ reconciliation 声明的上轮 ID(status !== fixed)——stuck ID 驱动数据源(5.1)
|
|
486
|
+
const reconEscalate = new Set(); // 5.1-5 escalate 声明:deferred 条目上下文改变 → 重新 open
|
|
487
|
+
const reconAll = new Set(); // M2: 所有 status 条目(含 fixed)的 prev_id 去重——reconcile 门控数据源
|
|
428
488
|
for (let i = 0; i < allRaw.length; i++) {
|
|
429
489
|
const raw = allRaw[i];
|
|
430
490
|
if (raw && typeof raw === "object" && raw.error) {
|
|
431
|
-
//
|
|
432
|
-
|
|
491
|
+
// 审查 agent 调用失败(含 AgentRegistry not found / 超时)。不裸 throw——与
|
|
492
|
+
// aggregator-failure/stuck/fix-failure 路径一致:saveState + terminated 结构化终止,
|
|
493
|
+
// 保证 state.json 有记录、调用方拿到结构化结果而非裸异常(MF-3)。
|
|
494
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
495
|
+
saveState(state);
|
|
496
|
+
terminated = "review-failure";
|
|
497
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 审查 agent 调用失败 " + active[i].name + " — " + raw.error;
|
|
498
|
+
batchIndex = BATCHES.length + 1; // 终止外层循环
|
|
499
|
+
break;
|
|
433
500
|
}
|
|
434
|
-
const parsed =
|
|
435
|
-
if (parsed
|
|
501
|
+
const parsed = normalizeReviewResult(raw.value);
|
|
502
|
+
if (parsed) {
|
|
503
|
+
// 5.8 通用落盘:schema-only agent(report_content 无 report_file,如 doc-reviewer)→
|
|
504
|
+
// workflow 写盘到 <roundDir>/<def.report>.md 并填入 report_file(aggregator 读取路径不变)。
|
|
505
|
+
const reportPath = resolveReviewReportPath(parsed, roundDir, active[i].report);
|
|
506
|
+
if (reportPath && !(parsed.report_file && parsed.report_file.trim())) {
|
|
507
|
+
try {
|
|
508
|
+
fs.writeFileSync(reportPath, parsed.report_content || "", "utf-8");
|
|
509
|
+
parsed.report_file = reportPath;
|
|
510
|
+
} catch (e) {
|
|
511
|
+
log("WARN: failed to write report_content to " + reportPath + " — " + e.message);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
436
514
|
reviewResults.push(parsed);
|
|
515
|
+
for (const r of parsed.reconciliation) {
|
|
516
|
+
if (r.status === "escalate") reconEscalate.add(r.prev_id);
|
|
517
|
+
else if (r.status !== "fixed") reconSeen.add(r.prev_id);
|
|
518
|
+
reconAll.add(r.prev_id); // M2: 含 fixed——全 fixed 时 reconSeen 空但 reconcile 仍需执行
|
|
519
|
+
}
|
|
437
520
|
const def = active[i];
|
|
438
521
|
if (parsed.must_fix === 0) {
|
|
439
522
|
recordAgentClean(state, def.name, batchIndex);
|
|
@@ -444,11 +527,18 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
444
527
|
agentRoundResults.push({ name: def.name, must_fix: parsed.must_fix, suggestion: parsed.suggestion ?? 0, clean: parsed.must_fix === 0 });
|
|
445
528
|
} else {
|
|
446
529
|
// tools 受限的 agent(如 tools: read)会过滤掉 structured-output → schema 失效,
|
|
447
|
-
// 结果缺 must_fix
|
|
448
|
-
|
|
530
|
+
// 结果缺 must_fix。结构化终止(MF-3),raw 完整 dump 便于定位。
|
|
531
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
532
|
+
saveState(state);
|
|
533
|
+
terminated = "review-failure";
|
|
534
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 审查 agent 结果无效(缺 must_fix) " + active[i].name + " raw=" + JSON.stringify(raw.value).slice(0, 400);
|
|
535
|
+
batchIndex = BATCHES.length + 1; // 终止外层循环
|
|
536
|
+
break;
|
|
449
537
|
}
|
|
450
538
|
}
|
|
451
539
|
|
|
540
|
+
if (terminated === "review-failure") break; // 已结构化终止,退出 round 循环(MF-3)
|
|
541
|
+
|
|
452
542
|
if (reviewResults.every((r) => r.must_fix === 0)) {
|
|
453
543
|
log("Batch " + batchIndex + " round " + round + ": all agents clean.");
|
|
454
544
|
batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [] });
|
|
@@ -459,63 +549,25 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
459
549
|
|
|
460
550
|
// ── Aggregate(内置 prompt,不依赖任何 agent.md) ─────────
|
|
461
551
|
const aggRaw = await agent({
|
|
462
|
-
prompt:
|
|
463
|
-
"Batch " + batchIndex + "/" + BATCHES.length + " Round " + round + "/" + maxRounds + " — AGGREGATE REVIEWS",
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
"Sub-review results: " + JSON.stringify(reviewResults, null, 2),
|
|
468
|
-
"outputDir: " + roundDir,
|
|
469
|
-
"",
|
|
470
|
-
"─── PART 1: WRITE FILE ───────────────────────────────────",
|
|
471
|
-
"Write the human-readable aggregated report to:",
|
|
472
|
-
roundDir + "/aggregated.md",
|
|
473
|
-
"",
|
|
474
|
-
"Top section MUST be:",
|
|
475
|
-
"```",
|
|
476
|
-
"## Summary",
|
|
477
|
-
"- Must-fix: <N>",
|
|
478
|
-
"- Suggestions: <N>",
|
|
479
|
-
"- Infos: <N>",
|
|
480
|
-
"- Dimensions reviewed: <comma-separated>",
|
|
481
|
-
"- Dedup: <N> duplicates removed",
|
|
482
|
-
"```",
|
|
483
|
-
"",
|
|
484
|
-
"Followed by tables of Must-Fix Issues, Suggestions, Infos, and a Conclusion section.",
|
|
485
|
-
"The format `- Must-fix: N` and `- Suggestions: N` is critical: a fallback parser depends on it.",
|
|
486
|
-
"",
|
|
487
|
-
"─── PART 2: RETURN JSON (CRITICAL — loop reads THIS) ─────",
|
|
488
|
-
"Your FINAL response MUST be a single JSON object and NOTHING ELSE.",
|
|
489
|
-
"",
|
|
490
|
-
"Required shape (exact field names, no aliases, no extras):",
|
|
491
|
-
"{",
|
|
492
|
-
' "report_file": "' + roundDir + '/aggregated.md",',
|
|
493
|
-
' "must_fix": <integer>,',
|
|
494
|
-
' "suggestion": <integer>',
|
|
495
|
-
"}",
|
|
496
|
-
"",
|
|
497
|
-
"STRICT RULES:",
|
|
498
|
-
"- Field names MUST be exactly: report_file, must_fix, suggestion",
|
|
499
|
-
"- must_fix and suggestion MUST be integers — NOT strings, NOT null, NOT undefined",
|
|
500
|
-
"- The JSON object MUST be the ONLY thing in your final response",
|
|
501
|
-
"- DO NOT wrap in markdown code fences, DO NOT add prose before/after",
|
|
502
|
-
"",
|
|
503
|
-
"─── SELF-CHECK before returning ──────────────────────────",
|
|
504
|
-
"1. Did you write " + roundDir + "/aggregated.md? If not, do it first.",
|
|
505
|
-
"2. Is must_fix in your JSON equal to the 'Must-fix: N' in your markdown?",
|
|
506
|
-
"3. Is your final response the bare JSON object, no fences, no prose?",
|
|
507
|
-
].join("\n"),
|
|
552
|
+
prompt: buildAggregatorPrompt({
|
|
553
|
+
header: "Batch " + batchIndex + "/" + BATCHES.length + " Round " + round + "/" + maxRounds + " — AGGREGATE REVIEWS",
|
|
554
|
+
round, max: maxRounds, roundDir,
|
|
555
|
+
reviewResults,
|
|
556
|
+
}),
|
|
508
557
|
model: MODEL,
|
|
509
558
|
schema: aggregatorSchema,
|
|
510
559
|
description: "aggregate",
|
|
511
560
|
timeoutMs: 1_800_000,
|
|
561
|
+
returnMeta: true,
|
|
512
562
|
});
|
|
513
563
|
|
|
514
|
-
|
|
564
|
+
// returnMeta 下 aggRaw = {value, error};失败时 value 为空串、error 在 finalMessage 透出(MF-1)
|
|
565
|
+
const aggValue = aggRaw?.value ?? aggRaw;
|
|
566
|
+
let agg = normalizeAggregatorResult(aggValue);
|
|
515
567
|
|
|
516
568
|
if (!agg || typeof agg.must_fix !== "number") {
|
|
517
|
-
const rawPreview = (typeof
|
|
518
|
-
log("Aggregator JSON invalid (len=" + (
|
|
569
|
+
const rawPreview = (aggValue === undefined ? "undefined" : typeof aggValue === "string" ? aggValue : JSON.stringify(aggValue)).slice(0, 200);
|
|
570
|
+
log("Aggregator JSON invalid (len=" + (typeof aggValue === "string" ? aggValue.length : 0) + "): " + rawPreview);
|
|
519
571
|
const fallbackPath = (agg && agg.report_file) || (roundDir + "/aggregated.md");
|
|
520
572
|
try {
|
|
521
573
|
const content = fs.readFileSync(fallbackPath, "utf-8");
|
|
@@ -531,7 +583,8 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
531
583
|
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
532
584
|
saveState(state);
|
|
533
585
|
terminated = "aggregator-failure";
|
|
534
|
-
finalMessage = "Batch " + batchIndex + " round " + round + ": aggregator 失败且 fallback 解析失败"
|
|
586
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": aggregator 失败且 fallback 解析失败"
|
|
587
|
+
+ (aggRaw && typeof aggRaw === "object" && aggRaw.error ? " — " + aggRaw.error : "");
|
|
535
588
|
batchIndex = BATCHES.length + 1; // 终止外层循环
|
|
536
589
|
break;
|
|
537
590
|
}
|
|
@@ -541,24 +594,159 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
541
594
|
const suggestion = agg.suggestion ?? 0;
|
|
542
595
|
log("Aggregated: " + mustFix + " must-fix + " + suggestion + " suggestion(s).");
|
|
543
596
|
|
|
597
|
+
// 5.1:R1 的 aggregator must_fix_ids 初始化 state.issues(数字 ID 起点)
|
|
598
|
+
if (round === 1 && agg.must_fix_ids && agg.must_fix_ids.length > 0) {
|
|
599
|
+
if (!state.issues) state.issues = {};
|
|
600
|
+
for (const entry of agg.must_fix_ids) {
|
|
601
|
+
const id = typeof entry === "string" ? entry : entry && entry.id;
|
|
602
|
+
if (!id || state.issues[id]) continue;
|
|
603
|
+
state.issues[id] = {
|
|
604
|
+
// severity 结构化(5.7):aggregator 标注 critical/major/minor,converged 终止的
|
|
605
|
+
// 「无 critical」判定依赖它;旧格式(string)默认 major(must-fix 语义)。
|
|
606
|
+
firstSeen: 1,
|
|
607
|
+
severity: typeof entry === "string" ? "major" : (entry.severity || "major"),
|
|
608
|
+
status: "open",
|
|
609
|
+
history: [{ round: 1, status: "open" }], fixAttempts: 0,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
544
614
|
// ── Stuck detection ─────────────────────────────────────
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
615
|
+
// 5.1 ID 驱动:R2+ 用 reconciliation 声明的 seenIds + reconcileIssues(同一 ID 连续 N 轮
|
|
616
|
+
// open/regressed);无 reconciliation 数据(R1 或 reviewer 未填)降级计数式 updateStuckState
|
|
617
|
+
// (现状语义,MF-2 注释保留)。needs-redesign(fixAttempts>=2)在 wave 5 接入。
|
|
618
|
+
// M2: reconCount = 所有 status 条目的 prev_id 去重计数(含 fixed)——全 fixed(最常见
|
|
619
|
+
// 收敛路径)时 reconSeen 为空,但 reconciliation 有数据仍须调 reconcileIssues:
|
|
620
|
+
// fix-attempted → fixed 的唯一转换点(否则 fix-attempted 永不转 fixed)
|
|
621
|
+
const reconCount = reconAll.size;
|
|
622
|
+
// F1: 无对账数据(doc-reviewer-only 批 reconciliation 恒空)时,fix-attempted 条目
|
|
623
|
+
// 存在仍须执行 reconcile——空 seenIds 语义 = 未被重新报告 = 已修复(5.1「未出现→fixed」)
|
|
624
|
+
const hasFixAttempted = state.issues
|
|
625
|
+
? Object.values(state.issues).some((i) => i.status === "fix-attempted")
|
|
626
|
+
: false;
|
|
627
|
+
|
|
628
|
+
// 5.1-2 R2+ 新发现 ID 契约(M2 移出 reconcile 分支,独立执行;F1 扩展为
|
|
629
|
+
// 「重新报告 = 未修复」转换):aggregator 的 must_fix_ids 中
|
|
630
|
+
// a) 不在 issues → 创建为新条目(firstSeen=round,severity 从 aggregator 标注)
|
|
631
|
+
// b) 已存在且(fix-attempted 或 fixed)且本轮无对账数据(reconCount===0,
|
|
632
|
+
// doc-reviewer 场景)→ 重新报告 = 修复失败:转 regressed + fixAttempts+1 + openStreak+1
|
|
633
|
+
// (RC-7 needs-redesign 出口在无对账配置下可达;reconciliation 场景由
|
|
634
|
+
// reconcileIssues 处理,避免双计)。fixed 重报分支(MF-2):已确认修复的问题
|
|
635
|
+
// 再次被报告同样转 regressed——否则 fixed 停留 + 收敛终止组合会在默认配置下
|
|
636
|
+
// R3 即以 converged 提前终止而 must-fix 仍活跃。
|
|
637
|
+
// newFindings 统计与 needs-redesign/fixAttempts 追踪对 R2+ 新发现生效。
|
|
638
|
+
if (round > 1 && state.issues && agg.must_fix_ids && agg.must_fix_ids.length > 0) {
|
|
639
|
+
let added = 0;
|
|
640
|
+
for (const entry of agg.must_fix_ids) {
|
|
641
|
+
const id = typeof entry === "string" ? entry : entry && entry.id;
|
|
642
|
+
if (!id) continue;
|
|
643
|
+
if (state.issues[id]) {
|
|
644
|
+
if (reconCount === 0 && (state.issues[id].status === "fix-attempted" || state.issues[id].status === "fixed")) {
|
|
645
|
+
state.issues[id].status = "regressed";
|
|
646
|
+
state.issues[id].fixAttempts = (state.issues[id].fixAttempts || 0) + 1;
|
|
647
|
+
state.issues[id].openStreak = (state.issues[id].openStreak || 0) + 1;
|
|
648
|
+
state.issues[id].severity = typeof entry === "string" ? "major" : (entry.severity || "major");
|
|
649
|
+
state.issues[id].history.push({ round, status: "regressed" });
|
|
650
|
+
added++;
|
|
651
|
+
}
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
state.issues[id] = {
|
|
655
|
+
firstSeen: round,
|
|
656
|
+
severity: typeof entry === "string" ? "major" : (entry.severity || "major"),
|
|
657
|
+
status: "open", openStreak: 1,
|
|
658
|
+
history: [{ round, status: "open" }], fixAttempts: 0,
|
|
659
|
+
};
|
|
660
|
+
added++;
|
|
661
|
+
}
|
|
662
|
+
if (added > 0) log("New findings tracked: " + added + " new or re-reported issue(s) in round " + round);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
let stuck = { stuck: false };
|
|
666
|
+
if (round > 1 && (reconCount > 0 || hasFixAttempted)) {
|
|
667
|
+
const rec = reconcileIssues(state.issues || {}, { seenIds: reconSeen, escalateIds: reconEscalate, round, stuckThreshold });
|
|
668
|
+
state.issues = rec.issues;
|
|
669
|
+
state.knownRemaining = rec.knownRemaining;
|
|
670
|
+
stuck = { stuck: rec.stuck, stuckIds: rec.stuckIds };
|
|
671
|
+
log("Reconcile: " + Object.keys(rec.issues).length + " tracked issue(s), known-remaining: " + rec.knownRemaining.length);
|
|
672
|
+
} else {
|
|
673
|
+
const s = updateStuckState(prevMustFix, stuckCount, mustFix, stuckThreshold);
|
|
674
|
+
stuckCount = s.stuckCount;
|
|
675
|
+
prevMustFix = s.prevMustFix;
|
|
676
|
+
stuck = { stuck: s.stuck };
|
|
677
|
+
}
|
|
678
|
+
if (stuck.stuck) {
|
|
679
|
+
const stuckIds = (stuck.stuckIds || []).join(", ");
|
|
680
|
+
log("Stuck: issue(s) not converging for " + stuckThreshold + " rounds: " + stuckIds + ". Stopping.");
|
|
681
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
682
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
683
|
+
saveState(state);
|
|
684
|
+
terminated = "stuck";
|
|
685
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 问题 " + stuckIds + " 连续 " + stuckThreshold + " 轮未收敛。残留: "
|
|
686
|
+
+ (state.knownRemaining && state.knownRemaining.length ? state.knownRemaining.join("; ") : "无 deferred");
|
|
687
|
+
batchIndex = BATCHES.length + 1;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// 5.7 needs-redesign(RC-7):fixAttempts >= maxFixAttempts 且 regressed → 终止。
|
|
692
|
+
// 顺序在 stuck 之后:stuck(一直在)信息更宏观,needs-redesign(修不好)更具体,先 stuck 后 redesign。
|
|
693
|
+
if (round > 1 && state.issues) {
|
|
694
|
+
const redesign = findNeedsRedesign(state.issues, maxFixAttempts);
|
|
695
|
+
if (redesign.length > 0) {
|
|
696
|
+
const ids = redesign.map((r) => r.issue_id).join(", ");
|
|
697
|
+
// 5.7 message 三要素:ID + 修复历史摘要 + 残留清单(history 全文随 state.json 落盘)
|
|
698
|
+
const historySummary = redesign.map((r) => {
|
|
699
|
+
const hist = (r.history || []).map((h) => "R" + h.round + ":" + h.status).join(" -> ");
|
|
700
|
+
return r.issue_id + " [" + (hist || "no history") + "]";
|
|
701
|
+
}).join("; ");
|
|
702
|
+
log("Needs redesign: " + ids + " not converging after " + maxFixAttempts + " fix attempts. Stopping.");
|
|
550
703
|
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
551
704
|
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
552
705
|
saveState(state);
|
|
553
|
-
terminated = "
|
|
554
|
-
finalMessage = "Batch " + batchIndex + " round " + round + ":
|
|
706
|
+
terminated = "needs-redesign";
|
|
707
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 问题 " + historySummary + " 经 " + maxFixAttempts
|
|
708
|
+
+ " 次修复仍未收敛,属于需要重新设计而非继续补丁的结构性问题,请人工介入。残留: "
|
|
709
|
+
+ (state.knownRemaining && state.knownRemaining.length ? state.knownRemaining.join("; ") : "无 deferred");
|
|
555
710
|
batchIndex = BATCHES.length + 1;
|
|
556
711
|
break;
|
|
557
712
|
}
|
|
558
|
-
|
|
559
|
-
|
|
713
|
+
|
|
714
|
+
// 5.7 新发现率收敛:连续 convergeRounds 轮新发现 <= convergeNewIssues → converged
|
|
715
|
+
const newIssues = Object.values(state.issues).filter((i) => i.firstSeen === round);
|
|
716
|
+
const newFindings = newIssues.length;
|
|
717
|
+
const newFindingsCritical = newIssues.filter((i) => i.severity === "critical").length;
|
|
718
|
+
const conv = checkConvergence({
|
|
719
|
+
prevStreak: state.convergeStreak || 0, newFindings, newFindingsCritical,
|
|
720
|
+
convergeNewIssues, convergeRounds,
|
|
721
|
+
});
|
|
722
|
+
state.convergeStreak = conv.streak;
|
|
723
|
+
// MF-2/S-21 收敛门槛:新发现率收敛 ≠ 问题已解决——必须同时满足「无 open/regressed
|
|
724
|
+
// 活跃条目」才允许 converged 终止。fixed 条目复发(reconcile/merge 已转 regressed)
|
|
725
|
+
// 后活跃条目存在 → 不收敛,继续修复循环(默认配置下 R3 复发不再提前终止)。
|
|
726
|
+
// issues 无追踪(aggregator 缺 must_fix_ids)时回退 mustFix===0 数字级判定,
|
|
727
|
+
// 避免 must_fix>0 照常收敛掩盖未处理问题(S-21)。
|
|
728
|
+
const trackedCount = Object.keys(state.issues || {}).length;
|
|
729
|
+
const activeIssues = Object.values(state.issues || {})
|
|
730
|
+
.filter((i) => i.status === "open" || i.status === "regressed");
|
|
731
|
+
const noActiveIssues = trackedCount === 0 ? mustFix === 0 : activeIssues.length === 0;
|
|
732
|
+
if (conv.converged && noActiveIssues) {
|
|
733
|
+
// MF-2 ④:converged 消息列出 open issue ID(对齐 max-rounds 的 remainingIds 逻辑)。
|
|
734
|
+
// 门槛保证正常路径此处为空;状态漂移时调用方仍能看到残留而非「无 deferred」误报。
|
|
735
|
+
const remainingIds = Object.entries(state.issues || {})
|
|
736
|
+
.filter(([, i]) => i.status !== "fixed" && i.status !== "deferred")
|
|
737
|
+
.map(([id]) => id);
|
|
738
|
+
log("Converged: new findings <= " + convergeNewIssues + " for " + convergeRounds + " rounds. Batch " + batchIndex + " done, proceeding to next batch.");
|
|
739
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
740
|
+
saveState(state);
|
|
741
|
+
terminated = "converged";
|
|
742
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 新发现率收敛(连续 " + convergeRounds
|
|
743
|
+
+ " 轮新问题 ≤" + convergeNewIssues + ")。残留: "
|
|
744
|
+
+ (remainingIds.length ? remainingIds.join(", ") : "无")
|
|
745
|
+
+ (state.knownRemaining && state.knownRemaining.length ? ";deferred: " + state.knownRemaining.join("; ") : "");
|
|
746
|
+
batchClean = true; // MF-1: 与 clean 一致,让外层 for 推进下一批(不跳过后续批次)
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
560
749
|
}
|
|
561
|
-
prevTotal = total;
|
|
562
750
|
|
|
563
751
|
// ── Fix ─────────────────────────────────────────────────
|
|
564
752
|
phase("Fix");
|
|
@@ -585,35 +773,44 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
585
773
|
: "- Do NOT commit. Leave the fixes in the working tree (autoCommit=false).";
|
|
586
774
|
|
|
587
775
|
const fxRaw = await agent({
|
|
588
|
-
prompt:
|
|
589
|
-
"Fix round " + round + " (batch " + batchIndex + ")
|
|
590
|
-
"",
|
|
591
|
-
"## Aggregated Review Report",
|
|
776
|
+
prompt: buildFixPrompt({
|
|
777
|
+
header: "Fix round " + round + " (batch " + batchIndex + ")",
|
|
592
778
|
reportContent,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
"- Apply the MINIMAL correct fix (no refactoring, no style changes)",
|
|
597
|
-
"- Verify each fix by reading the changed file afterwards",
|
|
598
|
-
fixPrompt,
|
|
779
|
+
fixPrompt: FIX_DEF && FIX_DEF.isCustom
|
|
780
|
+
? fixPrompt + "\n\nFixer specification (from agent file):\n" + FIX_DEF.systemPrompt
|
|
781
|
+
: fixPrompt,
|
|
599
782
|
commitInstr,
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
description: "fix",
|
|
783
|
+
caution: agg.fixes_caution && agg.fixes_caution.length ? agg.fixes_caution : [],
|
|
784
|
+
}),
|
|
785
|
+
schema: fixSchema,
|
|
786
|
+
// 与 buildReviewCall 的 model: MODEL || def.model 对齐:custom fixer.md 的
|
|
787
|
+
// frontmatter model 字段同样生效(之前丢弃了 FIX_DEF.model,只在 review 阶段消费)
|
|
788
|
+
model: MODEL || (FIX_DEF && FIX_DEF.model),
|
|
789
|
+
description: (FIX_DEF && FIX_DEF.name) || "fix",
|
|
790
|
+
// info #15: 显式 timeoutMs 与 review/aggregator 档位一致(fix 是写操作中最长阶段,
|
|
791
|
+
// 不依赖引擎默认值——引擎默认值变化不会悄然缩短 fix 预算)
|
|
792
|
+
timeoutMs: 1_800_000,
|
|
793
|
+
returnMeta: true,
|
|
794
|
+
...(FIX_DEF && !FIX_DEF.isCustom ? { agent: FIX_DEF.name } : {}),
|
|
613
795
|
});
|
|
614
796
|
|
|
615
|
-
|
|
616
|
-
if (
|
|
797
|
+
// returnMeta 下 fxRaw = {value, error}:先查 error(失败分支可达,MF-1),再对 value 做 parseResult
|
|
798
|
+
if (fxRaw && typeof fxRaw === "object" && fxRaw.error) {
|
|
799
|
+
// fix agent 调用失败(AgentRegistry not found / 超时等)。
|
|
800
|
+
// 与 review 路径(raw.error → review-failure)对齐:结构化终止而非静默当成功——
|
|
801
|
+
// 否则 fixed_count 缺失被 `?? mustFix` 回退,totalFixed 虚增且 must_fix 不降白跑轮次(MF-1)。
|
|
802
|
+
log("Fix agent failed, stopping.");
|
|
803
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
804
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
805
|
+
saveState(state);
|
|
806
|
+
terminated = "fix-failure";
|
|
807
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": fix agent 调用失败 — " + fxRaw.error;
|
|
808
|
+
batchIndex = BATCHES.length + 1;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
const fx = parseResult(fxRaw.value);
|
|
812
|
+
const fixResult = fx ? normalizeFixResult(fx) : null;
|
|
813
|
+
if (!fixResult) {
|
|
617
814
|
log("Fix agent failed, stopping.");
|
|
618
815
|
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
619
816
|
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
@@ -624,7 +821,83 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
624
821
|
break;
|
|
625
822
|
}
|
|
626
823
|
|
|
627
|
-
|
|
824
|
+
// ES3 硬校验(5.3 红线,恢复 mustFixIds 交叉校验——wave 3 后 agg.must_fix_ids
|
|
825
|
+
// 已是标准字段):(1) deferred 只允许 minor;(2) must-fix 必须全进 fixes[](漏修
|
|
826
|
+
// 判 violation)。任一违规 → fix-failure(结构化终止)
|
|
827
|
+
const es3Violations = validateFixResult(fixResult, agg.must_fix_ids);
|
|
828
|
+
if (es3Violations.length > 0) {
|
|
829
|
+
// m7: violation 分两类——deferred 非 minor / must-fix 漏修(must-fix-not-fixed),
|
|
830
|
+
// finalMessage 文案区分:统一文案会把漏修误报成 defer 违规,误导修复方向
|
|
831
|
+
const parts = es3Violations.map((v) =>
|
|
832
|
+
v.severity === "must-fix-not-fixed"
|
|
833
|
+
? "must-fix 未在 fixes[] 中修复(漏修)— " + v.issue_id
|
|
834
|
+
: "deferred 含非 minor 条目(must-fix 不得 defer)— " + v.issue_id + "(" + v.severity + ")"
|
|
835
|
+
);
|
|
836
|
+
log("ES3 violation: " + JSON.stringify(es3Violations));
|
|
837
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
838
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
839
|
+
saveState(state);
|
|
840
|
+
terminated = "fix-failure";
|
|
841
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": " + parts.join("; ");
|
|
842
|
+
batchIndex = BATCHES.length + 1;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ES2 软校验(5.3 证据标准):defer 理由过短/无实质 → warning 日志(不终止)
|
|
847
|
+
for (const d of fixResult.deferred) {
|
|
848
|
+
const reason = typeof d.reason === "string" ? d.reason : "";
|
|
849
|
+
if (reason.trim().length < 20) {
|
|
850
|
+
log("WARN: deferred reason too short / no concrete cost description: " + JSON.stringify(d));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// affected_files 并入 state.fixImpactFiles(5.3/5.5):recheck scope = modifiedFiles ∪ fixImpactFiles
|
|
855
|
+
const impactFiles = [];
|
|
856
|
+
for (const f of fixResult.fixes) {
|
|
857
|
+
if (Array.isArray(f.affected_files)) {
|
|
858
|
+
for (const af of f.affected_files) {
|
|
859
|
+
if (typeof af === "string" && af.trim() && !impactFiles.includes(af.trim())) impactFiles.push(af.trim());
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
state.fixImpactFiles = impactFiles;
|
|
864
|
+
|
|
865
|
+
// m4: 先初始化容器——aggregator JSON 无效走 parseAggregatedMd 回退时 agg 无
|
|
866
|
+
// must_fix_ids → R1 初始化跳过 → state.issues undefined;此处初始化保证 deferred
|
|
867
|
+
// 写入与 knownRemaining 同步链路生效(否则 knownRemaining 恒空,deferred 跨轮继承整链失效)
|
|
868
|
+
if (!state.issues) state.issues = {};
|
|
869
|
+
// 5.1:fix 结果标记 fix-attempted(ID 对账驱动)+ fixResults 落库(R2+ prompt 输入)
|
|
870
|
+
for (const f of fixResult.fixes) {
|
|
871
|
+
if (f && typeof f.issue_id === "string" && state.issues[f.issue_id]) {
|
|
872
|
+
state.issues[f.issue_id].status = "fix-attempted";
|
|
873
|
+
state.issues[f.issue_id].history.push({ round, status: "fix-attempted" });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// 5.3-4 deferred 写入 state.issues(known-remaining 跨轮继承链路):deferred 条目
|
|
877
|
+
// 以 status=deferred 入 issues,据此生成 knownRemaining 传给 R2+ prompt。
|
|
878
|
+
// 若 ID 已存在(曾被修复/降级)→ 更新状态 + reason;不存在(S-x minor)→ 新建。
|
|
879
|
+
for (const d of fixResult.deferred) {
|
|
880
|
+
if (!d || typeof d.issue_id !== "string" || !d.issue_id) continue;
|
|
881
|
+
const reason = typeof d.reason === "string" ? d.reason : "";
|
|
882
|
+
if (state.issues[d.issue_id]) {
|
|
883
|
+
state.issues[d.issue_id].status = "deferred";
|
|
884
|
+
state.issues[d.issue_id].deferredReason = reason;
|
|
885
|
+
state.issues[d.issue_id].history.push({ round, status: "deferred" });
|
|
886
|
+
} else {
|
|
887
|
+
state.issues[d.issue_id] = {
|
|
888
|
+
firstSeen: round, severity: "minor", status: "deferred",
|
|
889
|
+
deferredReason: reason,
|
|
890
|
+
history: [{ round, status: "deferred" }], fixAttempts: 0,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// known-remaining 同步更新:deferred 在本轮 fix 后即生效,R2+ prompt 立即消费
|
|
895
|
+
// (不依赖下轮 reconcile 才生成——否则滞后一轮,reviewer 本轮看不到 deferred 清单)
|
|
896
|
+
state.knownRemaining = computeKnownRemaining(state.issues);
|
|
897
|
+
if (!state.fixResults) state.fixResults = [];
|
|
898
|
+
state.fixResults.push(fixResult);
|
|
899
|
+
|
|
900
|
+
const fixedCount = fixResult.fixed_count ?? mustFix;
|
|
628
901
|
totalFixed += fixedCount;
|
|
629
902
|
state.fixCount++;
|
|
630
903
|
roundHasFix = true;
|
|
@@ -639,6 +912,9 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
639
912
|
} catch { /* empty */ }
|
|
640
913
|
}
|
|
641
914
|
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles });
|
|
915
|
+
// M4: 批内即时字段——下轮 scoped 分支(recheckAfterFix=true)从
|
|
916
|
+
// state.lastModifiedFiles 读本批 fix 的真实改动文件(git 实测兜底)
|
|
917
|
+
state.lastModifiedFiles = modifiedFiles;
|
|
642
918
|
saveState(state);
|
|
643
919
|
|
|
644
920
|
log("Fixed " + fixedCount + " issue(s). Total: " + totalFixed + ". Modified " + modifiedFiles.length + " file(s). Continuing...");
|
|
@@ -648,10 +924,19 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
|
648
924
|
|
|
649
925
|
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
650
926
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
927
|
+
terminated = resolveBatchTerminated(batchClean, terminated);
|
|
928
|
+
if (terminated === "max-rounds") {
|
|
929
|
+
// 该批达到 maxRounds 仍残留 must-fix → fail-fast,不进入后续批。
|
|
930
|
+
// 5.9 残留清单:未 fixed 的 issues(status != fixed/deferred)+ known-remaining。
|
|
931
|
+
const remainingIds = state.issues
|
|
932
|
+
? Object.entries(state.issues)
|
|
933
|
+
.filter(([, i]) => i.status !== "fixed" && i.status !== "deferred")
|
|
934
|
+
.map(([id]) => id)
|
|
935
|
+
: [];
|
|
936
|
+
finalMessage = "Batch " + batchIndex + " (" + BATCH_NAMES[batchIndex - 1] + ") 达到 maxRounds=" + maxRounds
|
|
937
|
+
+ " 仍有 must-fix,终止整个 workflow。残留: "
|
|
938
|
+
+ (remainingIds.length ? remainingIds.join(", ") : "(issues 未追踪)")
|
|
939
|
+
+ (state.knownRemaining && state.knownRemaining.length ? ";deferred: " + state.knownRemaining.join("; ") : "");
|
|
655
940
|
log(finalMessage);
|
|
656
941
|
saveState(state);
|
|
657
942
|
batchIndex = BATCHES.length + 1;
|
|
@@ -671,7 +956,15 @@ return {
|
|
|
671
956
|
targetType,
|
|
672
957
|
target,
|
|
673
958
|
runDir: RUN_ROOT,
|
|
959
|
+
// 5.9 terminated 透出:非 clean 时 message 含终止原因 + 残留 ID 清单 + deferred 理由
|
|
960
|
+
// (stuck/needs-redesign/converged/max-rounds/*-failure 均由 finalMessage 承载)。
|
|
961
|
+
// 渲染层特判(launcher 对 terminated 非 clean 的视觉区分)留 TODO:当前 tool 结果
|
|
962
|
+
// 已含完整 message,主 agent 可直接感知差异。
|
|
963
|
+
// 5.9 视觉区分:非 clean 终止加 [UNRESOLVED] 前缀(tool 结果即主 agent 可见层,
|
|
964
|
+
// launcher 透传 message——无需跨模块渲染特判,W5C3 决策更新)
|
|
674
965
|
message: terminated === "clean"
|
|
675
966
|
? "All batches clean. " + totalFixed + " issue(s) fixed total. State: " + STATE_FILE
|
|
676
|
-
:
|
|
967
|
+
: terminated === "converged"
|
|
968
|
+
? finalMessage + " " + totalFixed + " issue(s) fixed total. State: " + STATE_FILE
|
|
969
|
+
: "[UNRESOLVED] " + finalMessage + ". State: " + STATE_FILE,
|
|
677
970
|
};
|