@zhushanwen/pi-subagent-workflow 3.0.0 → 5.0.0-dev.0
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 +44 -1
- package/src/execution/__tests__/agent-registry.test.ts +7 -7
- package/src/interface/__tests__/detectors.test.ts +42 -0
- package/src/interface/tool-workflow.ts +22 -4
- 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/__tests__/worker-script-builder.test.ts +35 -0
- package/src/orchestration/script-lint.ts +235 -15
- package/src/orchestration/worker-script-builder.ts +4 -2
- package/workflows/README.md +21 -0
- package/workflows/review-fix-loop-utils.cjs +840 -0
- package/workflows/review-fix-loop.js +968 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
// review-fix-loop.js — 通用多批审查-修复循环(内置 workflow)
|
|
2
|
+
//
|
|
3
|
+
// 模式:多批(batch)串行,批内循环(round):并行 review → aggregate → fix → 重审。
|
|
4
|
+
// 批次用于表达前置依赖(fallow 静态分析等前置检查必须先完成,后续审查才有意义)。
|
|
5
|
+
// 批内某 agent 已无 must-fix(critical/major)则后续轮跳过,优化 token 效率。
|
|
6
|
+
//
|
|
7
|
+
// 用法:
|
|
8
|
+
// workflow run review-fix-loop --args targetType=git-diff target=main \
|
|
9
|
+
// batch1=fallow-scan batch2=reviewer autoCommit=true
|
|
10
|
+
// workflow run review-fix-loop --args targetType=file target=/path/to/doc.md \
|
|
11
|
+
// batch1=reviewer autoCommit=false
|
|
12
|
+
//
|
|
13
|
+
// ⚠️ 唯一带写操作的内置 workflow:fix 阶段会修改文件(autoCommit=true 时 commit)。
|
|
14
|
+
// ⚠️ lintScript 约束(本脚本已遵守):含 parallel() 入口,禁止 bare IIFE;
|
|
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。
|
|
22
|
+
|
|
23
|
+
const meta = {
|
|
24
|
+
name: "review-fix-loop",
|
|
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"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ── 参数解析 + 白名单校验(fail-fast) ────────────────────────────
|
|
30
|
+
|
|
31
|
+
function fail(msg) {
|
|
32
|
+
throw new Error("review-fix-loop: " + msg);
|
|
33
|
+
}
|
|
34
|
+
|
|
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
|
+
);
|
|
79
|
+
|
|
80
|
+
// 白名单校验:未知参数名(防 batchX 拼错如 batchl)→ 报错
|
|
81
|
+
for (const key of Object.keys($ARGS)) {
|
|
82
|
+
if (VALID_ARG_KEYS.has(key)) continue;
|
|
83
|
+
if (/^batch\d+$/.test(key)) continue;
|
|
84
|
+
fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/model/skipCleanAgents/recheckAfterFix/fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds)");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const targetType = $ARGS.targetType;
|
|
88
|
+
if (!TARGET_TYPES.includes(targetType)) {
|
|
89
|
+
fail("targetType 必填且必须是枚举之一: " + TARGET_TYPES.join("/") + "(实际: " + JSON.stringify(targetType) + ")");
|
|
90
|
+
}
|
|
91
|
+
const target = typeof $ARGS.target === "string" ? $ARGS.target.trim() : "";
|
|
92
|
+
if (!target) fail("target 必填(git-diff 时传 base ref 如 main;file 传路径;dir 传目录;text 传描述)");
|
|
93
|
+
|
|
94
|
+
const reviewPrompt = typeof $ARGS.reviewPrompt === "string" && $ARGS.reviewPrompt.trim()
|
|
95
|
+
? $ARGS.reviewPrompt.trim()
|
|
96
|
+
: "审查变更/目标是否存在:逻辑错误、边界条件、类型不安全、遗漏、回归风险、代码规范问题。发现问题分三级:critical(严重,必须修)/ major(重要,应当修)/ minor(轻微,建议修)。critical+major 计入 must_fix。";
|
|
97
|
+
const fixPrompt = typeof $ARGS.fixPrompt === "string" && $ARGS.fixPrompt.trim()
|
|
98
|
+
? $ARGS.fixPrompt.trim()
|
|
99
|
+
: "修复全部 must-fix 问题(critical/major)。最小正确修复,不做重构、不做风格改动。";
|
|
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);
|
|
119
|
+
const MODEL = typeof $ARGS.model === "string" && $ARGS.model.trim() ? $ARGS.model.trim() : undefined;
|
|
120
|
+
|
|
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);
|
|
128
|
+
} else {
|
|
129
|
+
log("WARN: git rev-parse " + target + " failed, falling back to ref for diff base: " + target);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
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);
|
|
137
|
+
|
|
138
|
+
// batchNames(数量校验)
|
|
139
|
+
const rawBatchNames = typeof $ARGS.batchNames === "string" && $ARGS.batchNames.trim()
|
|
140
|
+
? $ARGS.batchNames.split(",").map((s) => s.trim()).filter(Boolean)
|
|
141
|
+
: [];
|
|
142
|
+
const BATCH_NAMES = resolveBatchNames(rawBatchNames, BATCHES, fail);
|
|
143
|
+
|
|
144
|
+
// fallow-scan 只在 git-diff 类型下有意义
|
|
145
|
+
validateFallowScan(BATCHES, targetType, fail);
|
|
146
|
+
|
|
147
|
+
// ── Schemas ─────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
const reviewerSchema = {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
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)" },
|
|
154
|
+
must_fix: { type: "number", description: "Number of must-fix (critical+major) issues found" },
|
|
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
|
+
},
|
|
169
|
+
},
|
|
170
|
+
required: ["report_file", "must_fix", "suggestion"],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const aggregatorSchema = {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
report_file: { type: "string", description: "Absolute path to aggregated.md" },
|
|
177
|
+
must_fix: { type: "number", description: "Total must-fix after dedup across all dimensions" },
|
|
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
|
+
},
|
|
203
|
+
},
|
|
204
|
+
required: ["report_file", "must_fix", "suggestion"],
|
|
205
|
+
};
|
|
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
|
+
|
|
242
|
+
// ── Per-run isolation: runId-scoped directories ─────────────────────
|
|
243
|
+
|
|
244
|
+
const fs = require("fs");
|
|
245
|
+
const path = require("path");
|
|
246
|
+
const os = require("os");
|
|
247
|
+
|
|
248
|
+
const RUN_ID = ($ARGS._runId && typeof $ARGS._runId === "string") ? $ARGS._runId : "run-" + Date.now();
|
|
249
|
+
const RUN_ROOT = path.join(os.tmpdir(), "review-fix-loop", RUN_ID);
|
|
250
|
+
const STATE_FILE = RUN_ROOT + "/state.json";
|
|
251
|
+
|
|
252
|
+
fs.mkdirSync(RUN_ROOT, { recursive: true });
|
|
253
|
+
log("Run directory: " + RUN_ROOT);
|
|
254
|
+
|
|
255
|
+
// ── State management (persistent, atomic writes) ────────────────────
|
|
256
|
+
|
|
257
|
+
function loadState() {
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
|
|
260
|
+
} catch {
|
|
261
|
+
return {
|
|
262
|
+
meta: {
|
|
263
|
+
runId: RUN_ID, workspace: $WORKSPACE || "", model: MODEL || "(default)",
|
|
264
|
+
targetType, target, batches: BATCHES, startedAt: new Date().toISOString(),
|
|
265
|
+
},
|
|
266
|
+
agentStatus: {},
|
|
267
|
+
fixCount: 0,
|
|
268
|
+
batches: [],
|
|
269
|
+
fixResults: [],
|
|
270
|
+
issues: undefined,
|
|
271
|
+
knownRemaining: [],
|
|
272
|
+
convergeStreak: 0,
|
|
273
|
+
lastModifiedFiles: [],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function saveState(state) {
|
|
279
|
+
const tmp = STATE_FILE + ".tmp";
|
|
280
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
281
|
+
fs.renameSync(tmp, STATE_FILE);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// clean 记录(recordAgentClean/recordAgentDirty 与跨批跳过判定 shouldSkipAgent
|
|
285
|
+
// 在 review-fix-loop-utils.cjs,vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts)
|
|
286
|
+
|
|
287
|
+
// ── Agent defs(loadAgentMd/resolveAgentDefs 在 review-fix-loop-utils.cjs) ──
|
|
288
|
+
|
|
289
|
+
// ── Build review calls ──────────────────────────────────────────────
|
|
290
|
+
|
|
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;
|
|
300
|
+
}
|
|
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 : [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
|
|
307
|
+
const header = "Batch " + batchIndex + " Round " + round + "/" + max + " — " + BATCH_NAMES[batchIndex - 1];
|
|
308
|
+
const prevBatchesHint = batchIndex > 1
|
|
309
|
+
? "\nPrior batch reports (optional context): " + RUN_ROOT + "/batch-*/ (use read)"
|
|
310
|
+
: "";
|
|
311
|
+
const base = {
|
|
312
|
+
model: MODEL || def.model,
|
|
313
|
+
schema: reviewerSchema,
|
|
314
|
+
description: def.name,
|
|
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,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
if (def.isFallow) {
|
|
323
|
+
return {
|
|
324
|
+
...base,
|
|
325
|
+
prompt: [
|
|
326
|
+
header,
|
|
327
|
+
"",
|
|
328
|
+
"Fallow static-analysis pre-scan (tool-based, NOT a git-diff review).",
|
|
329
|
+
"",
|
|
330
|
+
"Steps:",
|
|
331
|
+
"1. Check if fallow is installed: `which fallow`",
|
|
332
|
+
"2. If NOT installed: write the report with a one-line note, must_fix=0, suggestion=0.",
|
|
333
|
+
"3. If installed, run: `fallow audit --base " + lockedBase.base + " --format json --quiet`",
|
|
334
|
+
"4. Extract: complexity hotspots, dead code, unused exports, circular deps",
|
|
335
|
+
"5. Classify findings: critical/major count into must_fix; minor into suggestion.",
|
|
336
|
+
"",
|
|
337
|
+
"output 路径:" + roundDir + "/" + def.report + ".md",
|
|
338
|
+
"Write report to: " + roundDir + "/" + def.report + ".md",
|
|
339
|
+
].join("\n"),
|
|
340
|
+
};
|
|
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
|
+
|
|
387
|
+
const spec = def.isCustom
|
|
388
|
+
? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt
|
|
389
|
+
: "";
|
|
390
|
+
return {
|
|
391
|
+
...base,
|
|
392
|
+
prompt: [
|
|
393
|
+
header,
|
|
394
|
+
"",
|
|
395
|
+
reviewInstruction + prevBatchesHint,
|
|
396
|
+
"",
|
|
397
|
+
"Review requirements:",
|
|
398
|
+
reviewPrompt + spec,
|
|
399
|
+
"",
|
|
400
|
+
"output 路径:" + roundDir + "/" + def.report + ".md",
|
|
401
|
+
"Write report to: " + roundDir + "/" + def.report + ".md",
|
|
402
|
+
].join("\n"),
|
|
403
|
+
agent: def.isCustom ? undefined : def.name,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// agent 名解析失败(AgentRegistry not found,报错文案 `Agent "${name}" not found.`)时,尝试 review- 前缀兜底
|
|
408
|
+
async function runReviewAgent(call) {
|
|
409
|
+
let raw = await agent(call);
|
|
410
|
+
if (raw && typeof raw === "object" && raw.error
|
|
411
|
+
&& shouldRetryWithReviewPrefix(raw.error, call.agent)) {
|
|
412
|
+
log("Agent not found: " + call.agent + " — retrying with review- prefix");
|
|
413
|
+
raw = await agent({ ...call, agent: "review-" + call.agent });
|
|
414
|
+
}
|
|
415
|
+
return raw;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Main loop: batches (serial) × rounds (per-batch) ────────────────
|
|
419
|
+
|
|
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 || "";
|
|
424
|
+
let totalFixed = 0;
|
|
425
|
+
let terminated = "clean";
|
|
426
|
+
let finalMessage = "";
|
|
427
|
+
|
|
428
|
+
for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
429
|
+
const defs = resolveAgentDefs(BATCHES[batchIndex - 1]);
|
|
430
|
+
const cleanNames = new Set();
|
|
431
|
+
let round = 0;
|
|
432
|
+
let prevMustFix = -1;
|
|
433
|
+
let stuckCount = 0;
|
|
434
|
+
let batchClean = false;
|
|
435
|
+
let roundHasFix = false; // recheckAfterFix 用:上轮是否有 fix
|
|
436
|
+
const batchRounds = [];
|
|
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
|
+
|
|
445
|
+
// 跨批跳过:agent 在更早批 clean 且此后无 fix → 本批不派发
|
|
446
|
+
if (batchIndex > 1) {
|
|
447
|
+
for (const def of defs) {
|
|
448
|
+
const s = state.agentStatus[def.name];
|
|
449
|
+
if (shouldSkipAgent(s, state.fixCount, batchIndex)) {
|
|
450
|
+
cleanNames.add(def.name);
|
|
451
|
+
log("Cross-batch skip: " + def.name + " (clean in batch " + s.lastCleanBatch + ", no fix since)");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
while (round < maxRounds) {
|
|
457
|
+
round++;
|
|
458
|
+
log("--- Batch " + batchIndex + "/" + BATCHES.length + " (" + BATCH_NAMES[batchIndex - 1] + ") Round " + round + "/" + maxRounds + " ---");
|
|
459
|
+
|
|
460
|
+
phase("Review");
|
|
461
|
+
const roundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + round;
|
|
462
|
+
fs.mkdirSync(roundDir, { recursive: true });
|
|
463
|
+
|
|
464
|
+
let active = defs.filter((def) => !(skipCleanAgents && cleanNames.has(def.name)));
|
|
465
|
+
let scopedClean = new Set(); // recheckAfterFix 重派时:上一轮 clean 的 agent 本轮走限定 prompt
|
|
466
|
+
if (recheckAfterFix && round > 1 && roundHasFix) {
|
|
467
|
+
scopedClean = new Set(cleanNames); // 重派前快照上一轮 clean 集合
|
|
468
|
+
active = defs; // fix 后重派全批(强回归模式)
|
|
469
|
+
cleanNames.clear();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (active.length === 0) {
|
|
473
|
+
log("All agents clean/skipped — batch " + batchIndex + " done.");
|
|
474
|
+
batchClean = true;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
log("Review: " + active.map((d) => d.name).join(", ") + " (" + active.length + " agent(s) in parallel)...");
|
|
479
|
+
const calls = active.map((def) => buildReviewCall(def, round, maxRounds, batchIndex, roundDir, scopedClean.has(def.name)));
|
|
480
|
+
const allRaw = await parallel(calls.map(runReviewAgent));
|
|
481
|
+
|
|
482
|
+
// per-agent 结果区分:parallel 结果与 calls 一一对应
|
|
483
|
+
const reviewResults = [];
|
|
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 门控数据源
|
|
488
|
+
for (let i = 0; i < allRaw.length; i++) {
|
|
489
|
+
const raw = allRaw[i];
|
|
490
|
+
if (raw && typeof raw === "object" && raw.error) {
|
|
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;
|
|
500
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
520
|
+
const def = active[i];
|
|
521
|
+
if (parsed.must_fix === 0) {
|
|
522
|
+
recordAgentClean(state, def.name, batchIndex);
|
|
523
|
+
cleanNames.add(def.name);
|
|
524
|
+
} else {
|
|
525
|
+
recordAgentDirty(state, def.name, parsed.must_fix, batchIndex);
|
|
526
|
+
}
|
|
527
|
+
agentRoundResults.push({ name: def.name, must_fix: parsed.must_fix, suggestion: parsed.suggestion ?? 0, clean: parsed.must_fix === 0 });
|
|
528
|
+
} else {
|
|
529
|
+
// tools 受限的 agent(如 tools: read)会过滤掉 structured-output → schema 失效,
|
|
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;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (terminated === "review-failure") break; // 已结构化终止,退出 round 循环(MF-3)
|
|
541
|
+
|
|
542
|
+
if (reviewResults.every((r) => r.must_fix === 0)) {
|
|
543
|
+
log("Batch " + batchIndex + " round " + round + ": all agents clean.");
|
|
544
|
+
batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [] });
|
|
545
|
+
saveState(state);
|
|
546
|
+
batchClean = true;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ── Aggregate(内置 prompt,不依赖任何 agent.md) ─────────
|
|
551
|
+
const aggRaw = await agent({
|
|
552
|
+
prompt: buildAggregatorPrompt({
|
|
553
|
+
header: "Batch " + batchIndex + "/" + BATCHES.length + " Round " + round + "/" + maxRounds + " — AGGREGATE REVIEWS",
|
|
554
|
+
round, max: maxRounds, roundDir,
|
|
555
|
+
reviewResults,
|
|
556
|
+
}),
|
|
557
|
+
model: MODEL,
|
|
558
|
+
schema: aggregatorSchema,
|
|
559
|
+
description: "aggregate",
|
|
560
|
+
timeoutMs: 1_800_000,
|
|
561
|
+
returnMeta: true,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// returnMeta 下 aggRaw = {value, error};失败时 value 为空串、error 在 finalMessage 透出(MF-1)
|
|
565
|
+
const aggValue = aggRaw?.value ?? aggRaw;
|
|
566
|
+
let agg = normalizeAggregatorResult(aggValue);
|
|
567
|
+
|
|
568
|
+
if (!agg || typeof agg.must_fix !== "number") {
|
|
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);
|
|
571
|
+
const fallbackPath = (agg && agg.report_file) || (roundDir + "/aggregated.md");
|
|
572
|
+
try {
|
|
573
|
+
const content = fs.readFileSync(fallbackPath, "utf-8");
|
|
574
|
+
const parsed = parseAggregatedMd(content);
|
|
575
|
+
if (parsed && typeof parsed.must_fix === "number") {
|
|
576
|
+
agg = { report_file: fallbackPath, must_fix: parsed.must_fix, suggestion: parsed.suggestion ?? 0 };
|
|
577
|
+
log("Fallback parsed from " + fallbackPath + ": must_fix=" + agg.must_fix);
|
|
578
|
+
}
|
|
579
|
+
} catch { /* fallback read failed */ }
|
|
580
|
+
|
|
581
|
+
if (!agg || typeof agg.must_fix !== "number") {
|
|
582
|
+
log("Aggregator failed and fallback failed, stopping.");
|
|
583
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
584
|
+
saveState(state);
|
|
585
|
+
terminated = "aggregator-failure";
|
|
586
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": aggregator 失败且 fallback 解析失败"
|
|
587
|
+
+ (aggRaw && typeof aggRaw === "object" && aggRaw.error ? " — " + aggRaw.error : "");
|
|
588
|
+
batchIndex = BATCHES.length + 1; // 终止外层循环
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const mustFix = agg.must_fix;
|
|
594
|
+
const suggestion = agg.suggestion ?? 0;
|
|
595
|
+
log("Aggregated: " + mustFix + " must-fix + " + suggestion + " suggestion(s).");
|
|
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
|
+
|
|
614
|
+
// ── Stuck detection ─────────────────────────────────────
|
|
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.");
|
|
703
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
704
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
705
|
+
saveState(state);
|
|
706
|
+
terminated = "needs-redesign";
|
|
707
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 问题 " + historySummary + " 经 " + maxFixAttempts
|
|
708
|
+
+ " 次修复仍未收敛,属于需要重新设计而非继续补丁的结构性问题,请人工介入。残留: "
|
|
709
|
+
+ (state.knownRemaining && state.knownRemaining.length ? state.knownRemaining.join("; ") : "无 deferred");
|
|
710
|
+
batchIndex = BATCHES.length + 1;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
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
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ── Fix ─────────────────────────────────────────────────
|
|
752
|
+
phase("Fix");
|
|
753
|
+
let reportContent;
|
|
754
|
+
try {
|
|
755
|
+
reportContent = fs.readFileSync(agg.report_file, "utf-8");
|
|
756
|
+
} catch {
|
|
757
|
+
reportContent = "(could not read aggregated report)";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let prevHead = "";
|
|
761
|
+
try {
|
|
762
|
+
prevHead = require("child_process").execSync(
|
|
763
|
+
"git rev-parse HEAD", { encoding: "utf-8", timeout: 10_000 }
|
|
764
|
+
).trim();
|
|
765
|
+
} catch {
|
|
766
|
+
// 非 git 项目(如纯文档目录):prevHead 为空,跳过 modifiedFiles 统计
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const commitInstr = autoCommit
|
|
770
|
+
? "- After all fixes, stage ONLY the files you modified: `git add <file1> <file2> ...` (explicit paths).\n" +
|
|
771
|
+
"- NEVER use `git add -A` or `git add .` — the workspace may contain unrelated untracked files.\n" +
|
|
772
|
+
"- Commit with message: `fix: review batch " + batchIndex + " round " + round + " — " + mustFix + " must-fix`"
|
|
773
|
+
: "- Do NOT commit. Leave the fixes in the working tree (autoCommit=false).";
|
|
774
|
+
|
|
775
|
+
const fxRaw = await agent({
|
|
776
|
+
prompt: buildFixPrompt({
|
|
777
|
+
header: "Fix round " + round + " (batch " + batchIndex + ")",
|
|
778
|
+
reportContent,
|
|
779
|
+
fixPrompt: FIX_DEF && FIX_DEF.isCustom
|
|
780
|
+
? fixPrompt + "\n\nFixer specification (from agent file):\n" + FIX_DEF.systemPrompt
|
|
781
|
+
: fixPrompt,
|
|
782
|
+
commitInstr,
|
|
783
|
+
caution: agg.fixes_caution && agg.fixes_caution.length ? agg.fixes_caution : [],
|
|
784
|
+
}),
|
|
785
|
+
schema: fixSchema,
|
|
786
|
+
model: MODEL,
|
|
787
|
+
description: "fix",
|
|
788
|
+
// info #15: 显式 timeoutMs 与 review/aggregator 档位一致(fix 是写操作中最长阶段,
|
|
789
|
+
// 不依赖引擎默认值——引擎默认值变化不会悄然缩短 fix 预算)
|
|
790
|
+
timeoutMs: 1_800_000,
|
|
791
|
+
returnMeta: true,
|
|
792
|
+
...(FIX_DEF && !FIX_DEF.isCustom ? { agent: FIX_DEF.name } : {}),
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// returnMeta 下 fxRaw = {value, error}:先查 error(失败分支可达,MF-1),再对 value 做 parseResult
|
|
796
|
+
if (fxRaw && typeof fxRaw === "object" && fxRaw.error) {
|
|
797
|
+
// fix agent 调用失败(AgentRegistry not found / 超时等)。
|
|
798
|
+
// 与 review 路径(raw.error → review-failure)对齐:结构化终止而非静默当成功——
|
|
799
|
+
// 否则 fixed_count 缺失被 `?? mustFix` 回退,totalFixed 虚增且 must_fix 不降白跑轮次(MF-1)。
|
|
800
|
+
log("Fix agent failed, stopping.");
|
|
801
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
802
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
803
|
+
saveState(state);
|
|
804
|
+
terminated = "fix-failure";
|
|
805
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": fix agent 调用失败 — " + fxRaw.error;
|
|
806
|
+
batchIndex = BATCHES.length + 1;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
const fx = parseResult(fxRaw.value);
|
|
810
|
+
const fixResult = fx ? normalizeFixResult(fx) : null;
|
|
811
|
+
if (!fixResult) {
|
|
812
|
+
log("Fix agent failed, stopping.");
|
|
813
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
814
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
815
|
+
saveState(state);
|
|
816
|
+
terminated = "fix-failure";
|
|
817
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": fix agent 结果无效";
|
|
818
|
+
batchIndex = BATCHES.length + 1;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ES3 硬校验(5.3 红线,恢复 mustFixIds 交叉校验——wave 3 后 agg.must_fix_ids
|
|
823
|
+
// 已是标准字段):(1) deferred 只允许 minor;(2) must-fix 必须全进 fixes[](漏修
|
|
824
|
+
// 判 violation)。任一违规 → fix-failure(结构化终止)
|
|
825
|
+
const es3Violations = validateFixResult(fixResult, agg.must_fix_ids);
|
|
826
|
+
if (es3Violations.length > 0) {
|
|
827
|
+
// m7: violation 分两类——deferred 非 minor / must-fix 漏修(must-fix-not-fixed),
|
|
828
|
+
// finalMessage 文案区分:统一文案会把漏修误报成 defer 违规,误导修复方向
|
|
829
|
+
const parts = es3Violations.map((v) =>
|
|
830
|
+
v.severity === "must-fix-not-fixed"
|
|
831
|
+
? "must-fix 未在 fixes[] 中修复(漏修)— " + v.issue_id
|
|
832
|
+
: "deferred 含非 minor 条目(must-fix 不得 defer)— " + v.issue_id + "(" + v.severity + ")"
|
|
833
|
+
);
|
|
834
|
+
log("ES3 violation: " + JSON.stringify(es3Violations));
|
|
835
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
|
|
836
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
837
|
+
saveState(state);
|
|
838
|
+
terminated = "fix-failure";
|
|
839
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": " + parts.join("; ");
|
|
840
|
+
batchIndex = BATCHES.length + 1;
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ES2 软校验(5.3 证据标准):defer 理由过短/无实质 → warning 日志(不终止)
|
|
845
|
+
for (const d of fixResult.deferred) {
|
|
846
|
+
const reason = typeof d.reason === "string" ? d.reason : "";
|
|
847
|
+
if (reason.trim().length < 20) {
|
|
848
|
+
log("WARN: deferred reason too short / no concrete cost description: " + JSON.stringify(d));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// affected_files 并入 state.fixImpactFiles(5.3/5.5):recheck scope = modifiedFiles ∪ fixImpactFiles
|
|
853
|
+
const impactFiles = [];
|
|
854
|
+
for (const f of fixResult.fixes) {
|
|
855
|
+
if (Array.isArray(f.affected_files)) {
|
|
856
|
+
for (const af of f.affected_files) {
|
|
857
|
+
if (typeof af === "string" && af.trim() && !impactFiles.includes(af.trim())) impactFiles.push(af.trim());
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
state.fixImpactFiles = impactFiles;
|
|
862
|
+
|
|
863
|
+
// m4: 先初始化容器——aggregator JSON 无效走 parseAggregatedMd 回退时 agg 无
|
|
864
|
+
// must_fix_ids → R1 初始化跳过 → state.issues undefined;此处初始化保证 deferred
|
|
865
|
+
// 写入与 knownRemaining 同步链路生效(否则 knownRemaining 恒空,deferred 跨轮继承整链失效)
|
|
866
|
+
if (!state.issues) state.issues = {};
|
|
867
|
+
// 5.1:fix 结果标记 fix-attempted(ID 对账驱动)+ fixResults 落库(R2+ prompt 输入)
|
|
868
|
+
for (const f of fixResult.fixes) {
|
|
869
|
+
if (f && typeof f.issue_id === "string" && state.issues[f.issue_id]) {
|
|
870
|
+
state.issues[f.issue_id].status = "fix-attempted";
|
|
871
|
+
state.issues[f.issue_id].history.push({ round, status: "fix-attempted" });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// 5.3-4 deferred 写入 state.issues(known-remaining 跨轮继承链路):deferred 条目
|
|
875
|
+
// 以 status=deferred 入 issues,据此生成 knownRemaining 传给 R2+ prompt。
|
|
876
|
+
// 若 ID 已存在(曾被修复/降级)→ 更新状态 + reason;不存在(S-x minor)→ 新建。
|
|
877
|
+
for (const d of fixResult.deferred) {
|
|
878
|
+
if (!d || typeof d.issue_id !== "string" || !d.issue_id) continue;
|
|
879
|
+
const reason = typeof d.reason === "string" ? d.reason : "";
|
|
880
|
+
if (state.issues[d.issue_id]) {
|
|
881
|
+
state.issues[d.issue_id].status = "deferred";
|
|
882
|
+
state.issues[d.issue_id].deferredReason = reason;
|
|
883
|
+
state.issues[d.issue_id].history.push({ round, status: "deferred" });
|
|
884
|
+
} else {
|
|
885
|
+
state.issues[d.issue_id] = {
|
|
886
|
+
firstSeen: round, severity: "minor", status: "deferred",
|
|
887
|
+
deferredReason: reason,
|
|
888
|
+
history: [{ round, status: "deferred" }], fixAttempts: 0,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// known-remaining 同步更新:deferred 在本轮 fix 后即生效,R2+ prompt 立即消费
|
|
893
|
+
// (不依赖下轮 reconcile 才生成——否则滞后一轮,reviewer 本轮看不到 deferred 清单)
|
|
894
|
+
state.knownRemaining = computeKnownRemaining(state.issues);
|
|
895
|
+
if (!state.fixResults) state.fixResults = [];
|
|
896
|
+
state.fixResults.push(fixResult);
|
|
897
|
+
|
|
898
|
+
const fixedCount = fixResult.fixed_count ?? mustFix;
|
|
899
|
+
totalFixed += fixedCount;
|
|
900
|
+
state.fixCount++;
|
|
901
|
+
roundHasFix = true;
|
|
902
|
+
|
|
903
|
+
let modifiedFiles = [];
|
|
904
|
+
if (prevHead) {
|
|
905
|
+
try {
|
|
906
|
+
const out = require("child_process").execSync(
|
|
907
|
+
"git diff --name-only " + prevHead, { encoding: "utf-8", timeout: 10_000 }
|
|
908
|
+
).trim();
|
|
909
|
+
modifiedFiles = out ? out.split("\n") : [];
|
|
910
|
+
} catch { /* empty */ }
|
|
911
|
+
}
|
|
912
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles });
|
|
913
|
+
// M4: 批内即时字段——下轮 scoped 分支(recheckAfterFix=true)从
|
|
914
|
+
// state.lastModifiedFiles 读本批 fix 的真实改动文件(git 实测兜底)
|
|
915
|
+
state.lastModifiedFiles = modifiedFiles;
|
|
916
|
+
saveState(state);
|
|
917
|
+
|
|
918
|
+
log("Fixed " + fixedCount + " issue(s). Total: " + totalFixed + ". Modified " + modifiedFiles.length + " file(s). Continuing...");
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (batchIndex > BATCHES.length) break; // 已终止
|
|
922
|
+
|
|
923
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
924
|
+
|
|
925
|
+
terminated = resolveBatchTerminated(batchClean, terminated);
|
|
926
|
+
if (terminated === "max-rounds") {
|
|
927
|
+
// 该批达到 maxRounds 仍残留 must-fix → fail-fast,不进入后续批。
|
|
928
|
+
// 5.9 残留清单:未 fixed 的 issues(status != fixed/deferred)+ known-remaining。
|
|
929
|
+
const remainingIds = state.issues
|
|
930
|
+
? Object.entries(state.issues)
|
|
931
|
+
.filter(([, i]) => i.status !== "fixed" && i.status !== "deferred")
|
|
932
|
+
.map(([id]) => id)
|
|
933
|
+
: [];
|
|
934
|
+
finalMessage = "Batch " + batchIndex + " (" + BATCH_NAMES[batchIndex - 1] + ") 达到 maxRounds=" + maxRounds
|
|
935
|
+
+ " 仍有 must-fix,终止整个 workflow。残留: "
|
|
936
|
+
+ (remainingIds.length ? remainingIds.join(", ") : "(issues 未追踪)")
|
|
937
|
+
+ (state.knownRemaining && state.knownRemaining.length ? ";deferred: " + state.knownRemaining.join("; ") : "");
|
|
938
|
+
log(finalMessage);
|
|
939
|
+
saveState(state);
|
|
940
|
+
batchIndex = BATCHES.length + 1;
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
saveState(state);
|
|
944
|
+
log("=== Batch " + batchIndex + " (" + BATCH_NAMES[batchIndex - 1] + ") CLEAN ===");
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
log("\n=== Loop Complete ===");
|
|
948
|
+
saveState(state);
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
batches: BATCHES.length,
|
|
952
|
+
totalFixed,
|
|
953
|
+
terminated,
|
|
954
|
+
targetType,
|
|
955
|
+
target,
|
|
956
|
+
runDir: RUN_ROOT,
|
|
957
|
+
// 5.9 terminated 透出:非 clean 时 message 含终止原因 + 残留 ID 清单 + deferred 理由
|
|
958
|
+
// (stuck/needs-redesign/converged/max-rounds/*-failure 均由 finalMessage 承载)。
|
|
959
|
+
// 渲染层特判(launcher 对 terminated 非 clean 的视觉区分)留 TODO:当前 tool 结果
|
|
960
|
+
// 已含完整 message,主 agent 可直接感知差异。
|
|
961
|
+
// 5.9 视觉区分:非 clean 终止加 [UNRESOLVED] 前缀(tool 结果即主 agent 可见层,
|
|
962
|
+
// launcher 透传 message——无需跨模块渲染特判,W5C3 决策更新)
|
|
963
|
+
message: terminated === "clean"
|
|
964
|
+
? "All batches clean. " + totalFixed + " issue(s) fixed total. State: " + STATE_FILE
|
|
965
|
+
: terminated === "converged"
|
|
966
|
+
? finalMessage + " " + totalFixed + " issue(s) fixed total. State: " + STATE_FILE
|
|
967
|
+
: "[UNRESOLVED] " + finalMessage + ". State: " + STATE_FILE,
|
|
968
|
+
};
|