@zhushanwen/pi-subagent-workflow 3.0.0 → 4.0.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.
@@ -2,7 +2,7 @@
2
2
  name: reviewer
3
3
  description: 代码审查 agent(diff 分析、问题发现)
4
4
  color: "#ef4444"
5
- tools: read
5
+ tools: read, structured-output
6
6
  ---
7
7
 
8
8
  You are a code reviewer. Your role is to find bugs, logic errors, and security issues.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -40,7 +40,7 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
- "@xyz-agent/extension-protocol": "^0.2.0",
43
+ "@xyz-agent/extension-protocol": "^0.3.0",
44
44
  "@zhushanwen/pi-extension-logger": "0.2.0"
45
45
  },
46
46
  "peerDependencies": {
@@ -54,7 +54,8 @@ const meta = { name: 'workflow-name', description: '...', phases: ['phase1', 'ph
54
54
  支持三种签名:
55
55
  - `agent(promptString)` — 最简,prompt 字符串,返回 content 字符串
56
56
  - `agent(promptString, { label?, schema?, ... })` — 字符串 + opts(`label` 是 `description` 的别名)
57
- - `agent({ prompt, schema?, description?, agent?, skill?, timeoutMs?, model?, scene? })` — 完整 opts 对象
57
+ - `agent({ prompt, schema?, description?, agent?, skill?, timeoutMs?, model?, scene?, thinkingLevel? })` — 完整 opts 对象
58
+ - `thinkingLevel?` (`string`) — 思考强度,合法值 `off | minimal | low | medium | high | xhigh`,透传至 pi CLI 拼接为 `--model provider/modelId:thinkingLevel`(与 `model?` 同效,影响子 agent 推理预算)
58
59
 
59
60
  Returns `parsedOutput` (structured data when schema provided) or `content` (string).
60
61
 
@@ -173,7 +173,7 @@ describe("createPackageBuiltinRegistry", () => {
173
173
  expect(builtin.get("orchestrator")?.tools).toEqual([
174
174
  "todo", "goal_control", "workflow", "subagent", "ask_user",
175
175
  ]);
176
- expect(builtin.get("reviewer")?.tools).toEqual(["read"]);
176
+ expect(builtin.get("reviewer")?.tools).toEqual(["read", "structured-output"]);
177
177
  expect(builtin.get("planner")?.tools).toEqual(["read"]);
178
178
  expect(builtin.get("oracle")?.tools).toEqual(["read"]);
179
179
  expect(builtin.get("context-builder")?.tools).toEqual(["read"]);
@@ -44,6 +44,20 @@ describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
44
44
  expect(findFlattenedArgKeys({ action: "status" })).toEqual([]);
45
45
  });
46
46
 
47
+ it("triggers for review-fix-loop args flattened to top level (incl. batchN prefix)", () => {
48
+ expect(
49
+ findFlattenedArgKeys({ action: "run", name: "review-fix-loop", targetType: "git-diff", target: "main" }),
50
+ ).toEqual(["targetType", "target"]);
51
+ expect(
52
+ findFlattenedArgKeys({ action: "run", name: "review-fix-loop", batch1: "reviewer", autoCommit: true }),
53
+ ).toEqual(["batch1", "autoCommit"]);
54
+ expect(
55
+ findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { targetType: "git-diff", target: "main" } }),
56
+ ).toEqual([]);
57
+ // 未知键(如 batchl 拼错)不属于白名单,不触发平铺检测(由 workflow 内白名单校验报错)
58
+ expect(findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" })).toEqual([]);
59
+ });
60
+
47
61
  it("returns [] for non-object input", () => {
48
62
  expect(findFlattenedArgKeys(null)).toEqual([]);
49
63
  expect(findFlattenedArgKeys(undefined)).toEqual([]);
@@ -99,7 +99,15 @@ const RUNID_SHORT = 8;
99
99
  /** 已知 workflow args 子字段——run action 的 args 顶层键。弱模型常把 task/items 等
100
100
  * 平铺到 workflow params 顶层(缺 args 嵌套),actionRun 静默 args={} 启动缺参 run(P0)。
101
101
  * 用此清单检测平铺形态,报错带 Correct 正例纠正。 */
102
- const KNOWN_ARG_KEYS = ["task", "target", "perspectives", "items", "itemsJson", "operation"];
102
+ const KNOWN_ARG_KEYS = [
103
+ "task", "target", "perspectives", "items", "itemsJson", "operation",
104
+ // review-fix-loop 参数(内置 workflow,2026-08 新增)
105
+ "targetType", "agents", "batchNames", "reviewPrompt", "fixPrompt",
106
+ "autoCommit", "maxRounds", "stuckThreshold", "skipCleanAgents", "recheckAfterFix",
107
+ ];
108
+
109
+ /** 前缀式参数(batch1..batchN 动态编号,无法枚举) */
110
+ const KNOWN_ARG_KEY_PREFIXES = [/^batch\d+$/];
103
111
 
104
112
  /**
105
113
  * 检测弱模型把 args 子字段平铺到 workflow params 顶层(P0 静默失败防护)。
@@ -110,7 +118,9 @@ export function findFlattenedArgKeys(params: unknown): string[] {
110
118
  if (typeof params !== "object" || params === null) return [];
111
119
  const p = params as Record<string, unknown>;
112
120
  const args = typeof p.args === "object" && p.args !== null ? p.args : undefined;
113
- return KNOWN_ARG_KEYS.filter((k) => k in p && !(args !== undefined && k in args));
121
+ const isKnownKey = (k: string) =>
122
+ KNOWN_ARG_KEYS.includes(k) || KNOWN_ARG_KEY_PREFIXES.some((re) => re.test(k));
123
+ return Object.keys(p).filter((k) => isKnownKey(k) && !(args !== undefined && k in args));
114
124
  }
115
125
 
116
126
  // ── Types ────────────────────────────────────────────────────
@@ -239,8 +249,11 @@ export function registerWorkflowTool(
239
249
  "chain (sequential 3-step: analyze→transform→synthesize; args: task), " +
240
250
  "parallel (multi-perspective analysis; args: target, optional perspectives), " +
241
251
  "scatter-gather (split→parallel→merge; args: task), " +
242
- "map-reduce (parallel map→reduce; args: items/itemsJson + operation). " +
243
- "Example: {\"action\":\"run\",\"name\":\"parallel\",\"args\":{\"target\":\"src/auth.ts\"}}.",
252
+ "map-reduce (parallel map→reduce; args: items/itemsJson + operation), " +
253
+ "review-fix-loop (multi-batch review→fix loop; args: targetType + target required, optional batch1..batchN). " +
254
+ "Example: {\"action\":\"run\",\"name\":\"parallel\",\"args\":{\"target\":\"src/auth.ts\"}}." +
255
+ "Use review-fix-loop when the user wants iterative code/doc review with fixes until clean " +
256
+ "(it is the ONLY built-in workflow that writes files; autoCommit defaults to false)." +
244
257
  "DISCOVERY: If unsure what workflows exist, call the workflow-script tool with " +
245
258
  "action:list first — it returns all available scripts (built-in + user-generated) " +
246
259
  "with source tags and descriptions. Then use this tool's run action to start one.",
@@ -222,3 +222,38 @@ describe("buildWorkerScript — W2 agent() returnMeta mode", () => {
222
222
  });
223
223
  });
224
224
 
225
+ // ── thinkingLevel 参数透传(agent() 三分支) ──
226
+ // 验证 thinkingLevel 在 string / task-agent / object.prompt 三个分支均被透传至
227
+ // agent-call 的 opts,且加入 _knownFields 白名单(object.prompt 分支整体透传,
228
+ // 不被 unknown-fields warn 误报)。底层 AgentCallOpts→mapToExecuteOptions→
229
+ // buildSpawnArgs 拼 pi CLI --model provider/modelId:thinkingLevel 已打通,
230
+ // 此处只验入口层 wiring。
231
+
232
+ describe("buildWorkerScript — agent() thinkingLevel passthrough (3 branches)", () => {
233
+ const script = buildWorkerScript("// noop user script");
234
+
235
+ it("string branch extracts thinkingLevel from secondArg (parity with model/scene/phase)", () => {
236
+ // agent("prompt", {thinkingLevel:"high"}) → string 分支 opts 提取 thinkingLevel
237
+ const stringBranch = script.match(/typeof firstArg === "string"[\s\S]*?\};/);
238
+ expect(stringBranch).toBeTruthy();
239
+ expect(stringBranch![0]).toContain(
240
+ "thinkingLevel: (secondArg && typeof secondArg === \"object\" && secondArg.thinkingLevel) || undefined",
241
+ );
242
+ });
243
+
244
+ it("task/agent branch includes thinkingLevel in opts (parity with model/skill/timeoutMs)", () => {
245
+ // agent({task, agent, thinkingLevel}) → task/agent 快捷分支透传 thinkingLevel
246
+ const taskAgentBranch = script.match(/firstArg\.task \|\| firstArg\.agent[\s\S]*?\};/);
247
+ expect(taskAgentBranch).toBeTruthy();
248
+ expect(taskAgentBranch![0]).toContain("thinkingLevel: firstArg.thinkingLevel");
249
+ });
250
+
251
+ it("thinkingLevel is a known field (object.prompt branch passes through without unknown-fields warn)", () => {
252
+ // object.prompt 分支 opts = firstArg 整体透传,thinkingLevel 自然带入;
253
+ // 但必须进 _knownFields 白名单,否则 Object.keys(opts) 含 thinkingLevel 会触发
254
+ // unknown-fields warn(workerLogs 污染)。验证 Set 与 warn 文案均识别 thinkingLevel。
255
+ expect(script).toContain('"thinkingLevel"');
256
+ expect(script).toMatch(/Known fields:.*thinkingLevel/);
257
+ });
258
+ });
259
+
@@ -191,6 +191,7 @@ export function buildWorkerScript(userScript: string): string {
191
191
  ' model: (secondArg && typeof secondArg === "object" && secondArg.model) || undefined,',
192
192
  ' scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,\n' +
193
193
  ' phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,',
194
+ ' thinkingLevel: (secondArg && typeof secondArg === "object" && secondArg.thinkingLevel) || undefined,',
194
195
  ' };',
195
196
  ' } else if (typeof firstArg === "object" && firstArg !== null) {',
196
197
  ' if (firstArg.prompt) {',
@@ -211,6 +212,7 @@ export function buildWorkerScript(userScript: string): string {
211
212
  ' fork: firstArg.fork,',
212
213
  ' worktree: firstArg.worktree,',
213
214
  ' returnMeta: firstArg.returnMeta,',
215
+ ' thinkingLevel: firstArg.thinkingLevel,',
214
216
  ' };',
215
217
  ' } else {',
216
218
  ' opts = firstArg;',
@@ -220,10 +222,10 @@ export function buildWorkerScript(userScript: string): string {
220
222
  ' }',
221
223
  '',
222
224
  ' // Validate known agent() fields to catch API misuse early',
223
- ' const _knownFields = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta"]);',
225
+ ' const _knownFields = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);',
224
226
  ' const _unknownFields = Object.keys(opts).filter((k) => !_knownFields.has(k));',
225
227
  ' if (_unknownFields.length > 0) {',
226
- ' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta"]);',
228
+ ' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel"]);',
227
229
  ' }',
228
230
  '',
229
231
  ' const callId = _callIdCounter;',
@@ -10,6 +10,9 @@
10
10
  | `parallel.js` | 多视角并行分析 → 聚合汇总 | `target`(可选 `perspectives`) | 多维度评估同一目标(安全/性能/可维护性等) |
11
11
  | `scatter-gather.js` | scatter 拆分 → parallel 处理 → gather 合并 | `task` | 大任务先拆成子任务再并行处理 |
12
12
  | `map-reduce.js` | parallel map → reduce 归约 | `items`/`itemsJson` + `operation` | 对已知数组批量变换后归约成单一结果 |
13
+ | `review-fix-loop.js` | 多批串行(批内并行 review → aggregate → fix → 重审) | `targetType` + `target`(可选 `batch1..batchN`) | 代码/文档审查并修复直到 clean;前置检查先行(fallow 等) |
14
+
15
+ > ⚠️ **review-fix-loop 是唯一带写操作的内置 workflow**(fix 阶段修改文件,`autoCommit=true` 才 commit)。其他 4 个均为只读分析。
13
16
 
14
17
  ## 用法
15
18
 
@@ -49,6 +52,21 @@ workflow run map-reduce --args itemsJson=/path/to/items.json --args operation=".
49
52
 
50
53
  `items` 直接传 JSON 数组,或 `itemsJson` 传 JSON 文件路径(二选一)。`parallel()` 对每个 item 并行执行 `operation` → reduce 阶段用 `agent()` 把各 item 的 map 结果归约成单一结论(LLM 归约,非纯代码拼接)。
51
54
 
55
+ ### review-fix-loop — 多批审查-修复循环
56
+
57
+ ```
58
+ workflow run review-fix-loop --args targetType=git-diff target=main \
59
+ --args batch1=fallow-scan --args batch2=reviewer --args autoCommit=true
60
+ workflow run review-fix-loop --args targetType=file target=/path/to/doc.md \
61
+ --args batch1=reviewer
62
+ ```
63
+
64
+ - `targetType` 枚举:`git-diff`(target=base ref)/ `file`(target=路径)/ `dir`(target=目录)/ `text`(target=自由描述)
65
+ - `batch1..batchN`:批串行,批内并行 review → aggregate → fix → 重审直到 clean;批次用于前置依赖(如 `fallow-scan` 静态分析先行,后续审查才有意义)
66
+ - 批内某 agent 无 must-fix 后后续轮跳过(`skipCleanAgents`,默认 true);`recheckAfterFix=true` 可在 fix 后重派全批做回归防护
67
+ - agent 项支持:AgentRegistry 名(如 `reviewer`)/ 自定义 .md 文件路径(如 `batch1=/path/to/reviewer.md`)/ 内置 `fallow-scan`
68
+ - ⚠️ **fix 阶段会修改文件;`autoCommit` 默认 false(不 commit)**,需要提交时显式 `autoCommit=true`
69
+
52
70
  ## 编排 API
53
71
 
54
72
  这些 workflow 内部使用的编排函数(`agent()` / `parallel()` / `pipeline()` / `workflow()`)由 worker 线程注入,完整 API 参考见 `skills/workflow-script-format/SKILL.md`。
@@ -0,0 +1,677 @@
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
+ const meta = {
18
+ 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
+ ],
24
+ };
25
+
26
+ // ── 参数解析 + 白名单校验(fail-fast) ────────────────────────────
27
+
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
+ function fail(msg) {
36
+ throw new Error("review-fix-loop: " + msg);
37
+ }
38
+
39
+ function normalizeBool(v, name, def) {
40
+ if (v === undefined || v === null || v === "") return def;
41
+ if (v === true || v === "true") return true;
42
+ if (v === false || v === "false") return false;
43
+ fail("参数 " + name + " 必须是布尔值(true/false),实际: " + JSON.stringify(v));
44
+ }
45
+
46
+ function normalizeInt(v, name, def) {
47
+ if (v === undefined || v === null || v === "") return def;
48
+ const n = typeof v === "number" ? v : Number(String(v).trim());
49
+ if (!Number.isInteger(n) || n <= 0) fail("参数 " + name + " 必须是正整数,实际: " + JSON.stringify(v));
50
+ return n;
51
+ }
52
+
53
+ // 白名单校验:未知参数名(防 batchX 拼错如 batchl)→ 报错
54
+ for (const key of Object.keys($ARGS)) {
55
+ if (VALID_ARG_KEYS.has(key)) continue;
56
+ if (/^batch\d+$/.test(key)) continue;
57
+ fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/model/skipCleanAgents/recheckAfterFix)");
58
+ }
59
+
60
+ const targetType = $ARGS.targetType;
61
+ if (!TARGET_TYPES.includes(targetType)) {
62
+ fail("targetType 必填且必须是枚举之一: " + TARGET_TYPES.join("/") + "(实际: " + JSON.stringify(targetType) + ")");
63
+ }
64
+ const target = typeof $ARGS.target === "string" ? $ARGS.target.trim() : "";
65
+ if (!target) fail("target 必填(git-diff 时传 base ref 如 main;file 传路径;dir 传目录;text 传描述)");
66
+
67
+ const reviewPrompt = typeof $ARGS.reviewPrompt === "string" && $ARGS.reviewPrompt.trim()
68
+ ? $ARGS.reviewPrompt.trim()
69
+ : "审查变更/目标是否存在:逻辑错误、边界条件、类型不安全、遗漏、回归风险、代码规范问题。发现问题分三级:critical(严重,必须修)/ major(重要,应当修)/ minor(轻微,建议修)。critical+major 计入 must_fix。";
70
+ const fixPrompt = typeof $ARGS.fixPrompt === "string" && $ARGS.fixPrompt.trim()
71
+ ? $ARGS.fixPrompt.trim()
72
+ : "修复全部 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
+ const recheckAfterFix = normalizeBool($ARGS.recheckAfterFix, "recheckAfterFix", false);
78
+ const MODEL = typeof $ARGS.model === "string" && $ARGS.model.trim() ? $ARGS.model.trim() : undefined;
79
+
80
+ // 审查指令模板(按 targetType 生成,注入每个 review agent 的 prompt)
81
+ function buildReviewInstruction() {
82
+ switch (targetType) {
83
+ case "git-diff":
84
+ return "Review `git diff " + target + "...HEAD` for all committed changes against " + target + ".\n" +
85
+ "ALSO run `git status --porcelain` and `git diff` to review uncommitted working-tree changes " +
86
+ "(fixes may be uncommitted when autoCommit=false; uncommitted changes ARE in scope).";
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];
115
+ } else {
116
+ rawBatches = ["reviewer"]; // 默认单批:包内置通用审查 agent
117
+ }
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
+ }
127
+ const BATCHES = parseBatches();
128
+
129
+ // batchNames(数量校验)
130
+ const rawBatchNames = typeof $ARGS.batchNames === "string" && $ARGS.batchNames.trim()
131
+ ? $ARGS.batchNames.split(",").map((s) => s.trim()).filter(Boolean)
132
+ : [];
133
+ if (rawBatchNames.length > 0 && rawBatchNames.length !== BATCHES.length) {
134
+ fail("batchNames 数量(" + rawBatchNames.length + ")必须与批数(" + BATCHES.length + ")一致");
135
+ }
136
+ const BATCH_NAMES = rawBatchNames.length ? rawBatchNames : BATCHES.map((_, i) => "batch-" + (i + 1));
137
+
138
+ // fallow-scan 只在 git-diff 类型下有意义
139
+ for (let i = 0; i < BATCHES.length; i++) {
140
+ if (BATCHES[i].includes("fallow-scan") && targetType !== "git-diff") {
141
+ fail("fallow-scan 只支持 targetType=git-diff(它审查 git 变更的静态分析),实际 targetType=" + targetType);
142
+ }
143
+ }
144
+
145
+ // ── Schemas ─────────────────────────────────────────────────────────
146
+
147
+ const reviewerSchema = {
148
+ type: "object",
149
+ properties: {
150
+ report_file: { type: "string", description: "Absolute path to the written review report (.md)" },
151
+ must_fix: { type: "number", description: "Number of must-fix (critical+major) issues found" },
152
+ suggestion: { type: "number", description: "Number of suggestion-level (minor) issues found" },
153
+ },
154
+ required: ["report_file", "must_fix", "suggestion"],
155
+ };
156
+
157
+ const aggregatorSchema = {
158
+ type: "object",
159
+ properties: {
160
+ report_file: { type: "string", description: "Absolute path to aggregated.md" },
161
+ must_fix: { type: "number", description: "Total must-fix after dedup across all dimensions" },
162
+ suggestion: { type: "number", description: "Total suggestions after dedup across all dimensions" },
163
+ },
164
+ required: ["report_file", "must_fix", "suggestion"],
165
+ };
166
+
167
+ // ── Per-run isolation: runId-scoped directories ─────────────────────
168
+
169
+ const fs = require("fs");
170
+ const path = require("path");
171
+ const os = require("os");
172
+
173
+ const RUN_ID = ($ARGS._runId && typeof $ARGS._runId === "string") ? $ARGS._runId : "run-" + Date.now();
174
+ const RUN_ROOT = path.join(os.tmpdir(), "review-fix-loop", RUN_ID);
175
+ const STATE_FILE = RUN_ROOT + "/state.json";
176
+
177
+ fs.mkdirSync(RUN_ROOT, { recursive: true });
178
+ log("Run directory: " + RUN_ROOT);
179
+
180
+ // ── State management (persistent, atomic writes) ────────────────────
181
+
182
+ function loadState() {
183
+ try {
184
+ return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
185
+ } catch {
186
+ return {
187
+ meta: {
188
+ runId: RUN_ID, workspace: $WORKSPACE || "", model: MODEL || "(default)",
189
+ targetType, target, batches: BATCHES, startedAt: new Date().toISOString(),
190
+ },
191
+ agentStatus: {},
192
+ fixCount: 0,
193
+ batches: [],
194
+ };
195
+ }
196
+ }
197
+
198
+ function saveState(state) {
199
+ const tmp = STATE_FILE + ".tmp";
200
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
201
+ fs.renameSync(tmp, STATE_FILE);
202
+ }
203
+
204
+ // clean 记录:lastCleanBatch + 当时全局 fixCount 快照(跨批跳过判定依据)
205
+ function recordAgentClean(state, agentName, batchIndex) {
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
+ }
237
+
238
+ function normalizeAggregatorResult(raw) {
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
+ }
252
+
253
+ function parseAggregatedMd(content) {
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
+ }
262
+
263
+ // 自定义 .md agent 加载:frontmatter(name/description/model)+ 正文
264
+ function loadAgentMd(filePath) {
265
+ let content;
266
+ try {
267
+ content = fs.readFileSync(filePath, "utf-8");
268
+ } catch (e) {
269
+ fail("agent 文件读取失败: " + filePath + " (" + e.message + ")");
270
+ }
271
+ let name = path.basename(filePath, ".md");
272
+ let model, description;
273
+ let body = content.trim();
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
+ });
305
+ }
306
+
307
+ // ── Build review calls ──────────────────────────────────────────────
308
+
309
+ function buildReviewCall(def, round, max, batchIndex, roundDir) {
310
+ const header = "Batch " + batchIndex + " Round " + round + "/" + max + " — " + BATCH_NAMES[batchIndex - 1];
311
+ const prevBatchesHint = batchIndex > 1
312
+ ? "\nPrior batch reports (optional context): " + RUN_ROOT + "/batch-*/ (use read)"
313
+ : "";
314
+ const base = {
315
+ model: MODEL || def.model,
316
+ schema: reviewerSchema,
317
+ description: def.name,
318
+ timeoutMs: 1_800_000,
319
+ };
320
+
321
+ if (def.isFallow) {
322
+ return {
323
+ ...base,
324
+ prompt: [
325
+ header,
326
+ "",
327
+ "Fallow static-analysis pre-scan (tool-based, NOT a git-diff review).",
328
+ "",
329
+ "Steps:",
330
+ "1. Check if fallow is installed: `which fallow`",
331
+ "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 " + target + " --format json --quiet`",
333
+ "4. Extract: complexity hotspots, dead code, unused exports, circular deps",
334
+ "5. Classify findings: critical/major count into must_fix; minor into suggestion.",
335
+ "",
336
+ "output 路径:" + roundDir + "/" + def.report + ".md",
337
+ "Write report to: " + roundDir + "/" + def.report + ".md",
338
+ ].join("\n"),
339
+ };
340
+ }
341
+
342
+ const spec = def.isCustom
343
+ ? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt
344
+ : "";
345
+ return {
346
+ ...base,
347
+ prompt: [
348
+ header,
349
+ "",
350
+ reviewInstruction + prevBatchesHint,
351
+ "",
352
+ "Review requirements:",
353
+ reviewPrompt + spec,
354
+ "",
355
+ "output 路径:" + roundDir + "/" + def.report + ".md",
356
+ "Write report to: " + roundDir + "/" + def.report + ".md",
357
+ ].join("\n"),
358
+ agent: def.isCustom ? undefined : def.name,
359
+ };
360
+ }
361
+
362
+ // agent 名解析失败(AgentRegistry not found)时,尝试 review- 前缀兜底
363
+ async function runReviewAgent(call) {
364
+ let raw = await agent(call);
365
+ if (raw && typeof raw === "object" && raw.error && typeof raw.error === "string"
366
+ && raw.error.includes("Agent not found") && call.agent && !call.agent.startsWith("review-")) {
367
+ log("Agent not found: " + call.agent + " — retrying with review- prefix");
368
+ raw = await agent({ ...call, agent: "review-" + call.agent });
369
+ }
370
+ return raw;
371
+ }
372
+
373
+ // ── Main loop: batches (serial) × rounds (per-batch) ────────────────
374
+
375
+ const state = loadState();
376
+ let totalFixed = 0;
377
+ let terminated = "clean";
378
+ let finalMessage = "";
379
+
380
+ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
381
+ const defs = resolveAgentDefs(BATCHES[batchIndex - 1]);
382
+ const cleanNames = new Set();
383
+ let round = 0;
384
+ let prevTotal = -1;
385
+ let stuckCount = 0;
386
+ let batchClean = false;
387
+ let roundHasFix = false; // recheckAfterFix 用:上轮是否有 fix
388
+ const batchRounds = [];
389
+
390
+ // 跨批跳过:agent 在更早批 clean 且此后无 fix → 本批不派发
391
+ if (batchIndex > 1) {
392
+ for (const def of defs) {
393
+ const s = state.agentStatus[def.name];
394
+ if (s && s.lastCleanBatch && s.lastCleanBatch < batchIndex && s.lastCleanFixCount === state.fixCount) {
395
+ cleanNames.add(def.name);
396
+ log("Cross-batch skip: " + def.name + " (clean in batch " + s.lastCleanBatch + ", no fix since)");
397
+ }
398
+ }
399
+ }
400
+
401
+ while (round < maxRounds) {
402
+ round++;
403
+ log("--- Batch " + batchIndex + "/" + BATCHES.length + " (" + BATCH_NAMES[batchIndex - 1] + ") Round " + round + "/" + maxRounds + " ---");
404
+
405
+ phase("Review");
406
+ const roundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + round;
407
+ fs.mkdirSync(roundDir, { recursive: true });
408
+
409
+ let active = defs.filter((def) => !(skipCleanAgents && cleanNames.has(def.name)));
410
+ if (recheckAfterFix && round > 1 && roundHasFix) {
411
+ active = defs; // fix 后重派全批(回归防护)
412
+ cleanNames.clear();
413
+ }
414
+
415
+ if (active.length === 0) {
416
+ log("All agents clean/skipped — batch " + batchIndex + " done.");
417
+ batchClean = true;
418
+ break;
419
+ }
420
+
421
+ 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);
424
+
425
+ // per-agent 结果区分:parallel 结果与 calls 一一对应
426
+ const reviewResults = [];
427
+ const agentRoundResults = [];
428
+ for (let i = 0; i < allRaw.length; i++) {
429
+ const raw = allRaw[i];
430
+ if (raw && typeof raw === "object" && raw.error) {
431
+ // fail-fast:审查 agent 调用失败(含 AgentRegistry not found)不得静默跳过
432
+ fail("审查 agent 调用失败: " + active[i].name + " — " + raw.error);
433
+ }
434
+ const parsed = parseResult(raw);
435
+ if (parsed && typeof parsed.must_fix === "number") {
436
+ reviewResults.push(parsed);
437
+ const def = active[i];
438
+ if (parsed.must_fix === 0) {
439
+ recordAgentClean(state, def.name, batchIndex);
440
+ cleanNames.add(def.name);
441
+ } else {
442
+ recordAgentDirty(state, def.name, parsed.must_fix, batchIndex);
443
+ }
444
+ agentRoundResults.push({ name: def.name, must_fix: parsed.must_fix, suggestion: parsed.suggestion ?? 0, clean: parsed.must_fix === 0 });
445
+ } else {
446
+ // tools 受限的 agent(如 tools: read)会过滤掉 structured-output → schema 失效,
447
+ // 结果缺 must_fix。fail 信息完整 dump raw 便于定位。
448
+ fail("审查 agent 结果无效(缺 must_fix): " + active[i].name + " raw=" + JSON.stringify(raw).slice(0, 400));
449
+ }
450
+ }
451
+
452
+ if (reviewResults.every((r) => r.must_fix === 0)) {
453
+ log("Batch " + batchIndex + " round " + round + ": all agents clean.");
454
+ batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [] });
455
+ saveState(state);
456
+ batchClean = true;
457
+ break;
458
+ }
459
+
460
+ // ── Aggregate(内置 prompt,不依赖任何 agent.md) ─────────
461
+ const aggRaw = await agent({
462
+ prompt: [
463
+ "Batch " + batchIndex + "/" + BATCHES.length + " Round " + round + "/" + maxRounds + " — AGGREGATE REVIEWS",
464
+ "",
465
+ "You have TWO outputs: (1) a markdown report file and (2) a JSON return value.",
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"),
508
+ model: MODEL,
509
+ schema: aggregatorSchema,
510
+ description: "aggregate",
511
+ timeoutMs: 1_800_000,
512
+ });
513
+
514
+ let agg = normalizeAggregatorResult(aggRaw);
515
+
516
+ if (!agg || typeof agg.must_fix !== "number") {
517
+ const rawPreview = (typeof aggRaw === "string" ? aggRaw : JSON.stringify(aggRaw)).slice(0, 200);
518
+ log("Aggregator JSON invalid (len=" + (aggRaw?.length ?? 0) + "): " + rawPreview);
519
+ const fallbackPath = (agg && agg.report_file) || (roundDir + "/aggregated.md");
520
+ try {
521
+ const content = fs.readFileSync(fallbackPath, "utf-8");
522
+ const parsed = parseAggregatedMd(content);
523
+ if (parsed && typeof parsed.must_fix === "number") {
524
+ agg = { report_file: fallbackPath, must_fix: parsed.must_fix, suggestion: parsed.suggestion ?? 0 };
525
+ log("Fallback parsed from " + fallbackPath + ": must_fix=" + agg.must_fix);
526
+ }
527
+ } catch { /* fallback read failed */ }
528
+
529
+ if (!agg || typeof agg.must_fix !== "number") {
530
+ log("Aggregator failed and fallback failed, stopping.");
531
+ state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
532
+ saveState(state);
533
+ terminated = "aggregator-failure";
534
+ finalMessage = "Batch " + batchIndex + " round " + round + ": aggregator 失败且 fallback 解析失败";
535
+ batchIndex = BATCHES.length + 1; // 终止外层循环
536
+ break;
537
+ }
538
+ }
539
+
540
+ const mustFix = agg.must_fix;
541
+ const suggestion = agg.suggestion ?? 0;
542
+ log("Aggregated: " + mustFix + " must-fix + " + suggestion + " suggestion(s).");
543
+
544
+ // ── Stuck detection ─────────────────────────────────────
545
+ const total = mustFix + suggestion;
546
+ if (prevTotal >= 0 && total >= prevTotal) {
547
+ stuckCount++;
548
+ if (stuckCount >= stuckThreshold) {
549
+ log("Stuck: total issues not decreasing for " + stuckThreshold + " rounds. Stopping.");
550
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
551
+ state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
552
+ saveState(state);
553
+ terminated = "stuck";
554
+ finalMessage = "Batch " + batchIndex + " round " + round + ": 总问题数连续 " + stuckThreshold + " 轮不下降";
555
+ batchIndex = BATCHES.length + 1;
556
+ break;
557
+ }
558
+ } else {
559
+ stuckCount = 0;
560
+ }
561
+ prevTotal = total;
562
+
563
+ // ── Fix ─────────────────────────────────────────────────
564
+ phase("Fix");
565
+ let reportContent;
566
+ try {
567
+ reportContent = fs.readFileSync(agg.report_file, "utf-8");
568
+ } catch {
569
+ reportContent = "(could not read aggregated report)";
570
+ }
571
+
572
+ let prevHead = "";
573
+ try {
574
+ prevHead = require("child_process").execSync(
575
+ "git rev-parse HEAD", { encoding: "utf-8", timeout: 10_000 }
576
+ ).trim();
577
+ } catch {
578
+ // 非 git 项目(如纯文档目录):prevHead 为空,跳过 modifiedFiles 统计
579
+ }
580
+
581
+ const commitInstr = autoCommit
582
+ ? "- After all fixes, stage ONLY the files you modified: `git add <file1> <file2> ...` (explicit paths).\n" +
583
+ "- NEVER use `git add -A` or `git add .` — the workspace may contain unrelated untracked files.\n" +
584
+ "- Commit with message: `fix: review batch " + batchIndex + " round " + round + " — " + mustFix + " must-fix`"
585
+ : "- Do NOT commit. Leave the fixes in the working tree (autoCommit=false).";
586
+
587
+ const fxRaw = await agent({
588
+ prompt: [
589
+ "Fix round " + round + " (batch " + batchIndex + "): Fix ALL must-fix issues from the aggregated review report below.",
590
+ "",
591
+ "## Aggregated Review Report",
592
+ reportContent,
593
+ "",
594
+ "## Instructions",
595
+ "- Fix every must-fix issue listed in the report",
596
+ "- Apply the MINIMAL correct fix (no refactoring, no style changes)",
597
+ "- Verify each fix by reading the changed file afterwards",
598
+ fixPrompt,
599
+ commitInstr,
600
+ "",
601
+ "Return the count of issues fixed.",
602
+ ].join("\n"),
603
+ schema: {
604
+ type: "object",
605
+ properties: {
606
+ fixed_count: { type: "number", description: "Number of issues fixed" },
607
+ fixes: { type: "array", items: { type: "string" }, description: "One-line description of each fix" },
608
+ },
609
+ required: ["fixed_count"],
610
+ },
611
+ model: MODEL,
612
+ description: "fix",
613
+ });
614
+
615
+ const fx = parseResult(fxRaw);
616
+ if (!fx) {
617
+ log("Fix agent failed, stopping.");
618
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
619
+ state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
620
+ saveState(state);
621
+ terminated = "fix-failure";
622
+ finalMessage = "Batch " + batchIndex + " round " + round + ": fix agent 结果无效";
623
+ batchIndex = BATCHES.length + 1;
624
+ break;
625
+ }
626
+
627
+ const fixedCount = fx.fixed_count ?? mustFix;
628
+ totalFixed += fixedCount;
629
+ state.fixCount++;
630
+ roundHasFix = true;
631
+
632
+ let modifiedFiles = [];
633
+ if (prevHead) {
634
+ try {
635
+ const out = require("child_process").execSync(
636
+ "git diff --name-only " + prevHead, { encoding: "utf-8", timeout: 10_000 }
637
+ ).trim();
638
+ modifiedFiles = out ? out.split("\n") : [];
639
+ } catch { /* empty */ }
640
+ }
641
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles });
642
+ saveState(state);
643
+
644
+ log("Fixed " + fixedCount + " issue(s). Total: " + totalFixed + ". Modified " + modifiedFiles.length + " file(s). Continuing...");
645
+ }
646
+
647
+ if (batchIndex > BATCHES.length) break; // 已终止
648
+
649
+ state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
650
+
651
+ if (!batchClean) {
652
+ // 该批达到 maxRounds 仍残留 must-fix → fail-fast,不进入后续批
653
+ terminated = "max-rounds";
654
+ finalMessage = "Batch " + batchIndex + " (" + BATCH_NAMES[batchIndex - 1] + ") 达到 maxRounds=" + maxRounds + " 仍有 must-fix,终止整个 workflow";
655
+ log(finalMessage);
656
+ saveState(state);
657
+ batchIndex = BATCHES.length + 1;
658
+ break;
659
+ }
660
+ saveState(state);
661
+ log("=== Batch " + batchIndex + " (" + BATCH_NAMES[batchIndex - 1] + ") CLEAN ===");
662
+ }
663
+
664
+ log("\n=== Loop Complete ===");
665
+ saveState(state);
666
+
667
+ return {
668
+ batches: BATCHES.length,
669
+ totalFixed,
670
+ terminated,
671
+ targetType,
672
+ target,
673
+ runDir: RUN_ROOT,
674
+ message: terminated === "clean"
675
+ ? "All batches clean. " + totalFixed + " issue(s) fixed total. State: " + STATE_FILE
676
+ : finalMessage + ". State: " + STATE_FILE,
677
+ };