@zhushanwen/pi-subagent-workflow 5.0.2 → 7.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.
Files changed (77) hide show
  1. package/agents/{reviewer.md → code-reviewer.md} +20 -3
  2. package/agents/context-builder.md +5 -0
  3. package/agents/doc-reviewer.md +9 -2
  4. package/agents/explorer.md +5 -0
  5. package/agents/general-purpose.md +5 -0
  6. package/agents/oracle.md +20 -4
  7. package/agents/orchestrator.md +6 -1
  8. package/agents/planner.md +5 -0
  9. package/agents/researcher.md +5 -0
  10. package/agents/worker.md +5 -0
  11. package/package.json +6 -4
  12. package/src/execution/__tests__/agent-registry.test.ts +189 -119
  13. package/src/execution/__tests__/crash-recovery.test.ts +0 -1
  14. package/src/execution/__tests__/execute-options-mapper.test.ts +4 -4
  15. package/src/execution/__tests__/index-session-start.test.ts +0 -1
  16. package/src/execution/__tests__/model-resolver.test.ts +20 -0
  17. package/src/execution/__tests__/session-start-reaper.test.ts +0 -2
  18. package/src/execution/__tests__/subprocess-agent-runner.test.ts +1 -1
  19. package/src/execution/agent-registry.ts +92 -169
  20. package/src/execution/execute-options-mapper.ts +2 -2
  21. package/src/execution/model-config-service.ts +13 -34
  22. package/src/execution/model-resolver.ts +5 -3
  23. package/src/execution/subagent-service.ts +9 -6
  24. package/src/execution/subprocess-agent-runner.ts +3 -2
  25. package/src/index.ts +4 -25
  26. package/src/injectors/__tests__/subagent-list-injector.test.ts +266 -14
  27. package/src/injectors/__tests__/workflow-list-injector.test.ts +236 -32
  28. package/src/injectors/subagent-list-injector.ts +99 -48
  29. package/src/injectors/workflow-list-injector.ts +65 -50
  30. package/src/interface/__tests__/detectors.test.ts +100 -43
  31. package/src/interface/__tests__/subagent-tool-prompt.test.ts +8 -12
  32. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +163 -0
  33. package/src/interface/__tests__/workflow-tool-prompt.test.ts +55 -9
  34. package/src/interface/subagent-tool.ts +7 -4
  35. package/src/interface/tool-workflow-script.ts +27 -10
  36. package/src/interface/tool-workflow.ts +174 -81
  37. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  38. package/src/orchestration/__tests__/config-loader.test.ts +124 -40
  39. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +33 -2
  40. package/src/orchestration/__tests__/lifecycle.test.ts +59 -2
  41. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +116 -51
  42. package/src/orchestration/__tests__/script-lint.test.ts +167 -1
  43. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  44. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +69 -0
  45. package/src/orchestration/__tests__/worker-script-builder.test.ts +51 -1
  46. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +2 -2
  47. package/src/orchestration/__tests__/workflows-e2e.test.ts +177 -24
  48. package/src/orchestration/agent-opts-resolver.ts +51 -101
  49. package/src/orchestration/args-validator.ts +127 -0
  50. package/src/orchestration/config-loader.ts +63 -94
  51. package/src/orchestration/error-recovery.ts +6 -9
  52. package/src/orchestration/launcher.ts +77 -41
  53. package/src/orchestration/lifecycle.ts +7 -0
  54. package/src/orchestration/models/ports.ts +0 -14
  55. package/src/orchestration/models/run-spec.ts +24 -1
  56. package/src/orchestration/models/types.ts +15 -8
  57. package/src/orchestration/models/workflow-script-registry.ts +3 -0
  58. package/src/orchestration/models/workflow-script.ts +10 -14
  59. package/src/orchestration/script-lint.ts +159 -0
  60. package/src/orchestration/worker-host.ts +5 -0
  61. package/src/orchestration/worker-script-builder.ts +15 -4
  62. package/src/orchestration/workflow-script-registry-impl.ts +34 -29
  63. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  64. package/src/shared/__tests__/resource-discovery.test.ts +167 -7
  65. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  66. package/src/shared/agent-ref.ts +36 -0
  67. package/src/shared/meta-parser.ts +257 -0
  68. package/src/shared/resource-discovery.ts +88 -2
  69. package/src/shared/resource-meta.ts +60 -0
  70. package/workflows/README.md +2 -2
  71. package/workflows/_shared/agent-refs.cjs +40 -0
  72. package/workflows/chain.js +30 -6
  73. package/workflows/map-reduce.js +33 -5
  74. package/workflows/parallel.js +34 -7
  75. package/workflows/review-fix-loop-utils.cjs +23 -103
  76. package/workflows/review-fix-loop.js +121 -58
  77. package/workflows/scatter-gather.js +30 -6
@@ -14,11 +14,23 @@
14
14
  //
15
15
  // ⚠️ lintScript 约束(本脚本已遵守):含 parallel() 入口,禁止 bare IIFE
16
16
 
17
- const meta = {
18
- name: "parallel",
19
- description: "通用编排:多视角并行分析同一目标,再聚合汇总",
20
- phases: ["parallel-analyze", "aggregate"],
21
- };
17
+ /* @pi-meta
18
+ name: parallel
19
+ description: 通用编排:多视角并行分析同一目标,再聚合汇总
20
+ phases: [parallel-analyze, aggregate]
21
+ parameters:
22
+ type: object
23
+ properties:
24
+ target: { type: string }
25
+ perspectives: { type: array, items: { type: string } }
26
+ agents: { type: string }
27
+ required: [target]
28
+ usage: |
29
+ ## 使用说明
30
+ - 多视角并行分析后聚合;perspectives 缺省用 3 个默认视角
31
+ - agents:逗号分隔的 agent .md 绝对路径;1 个应用于所有视角,N 个一一对应视角数
32
+ - 示例:workflow run parallel --args target="<分析目标>" agents="/path/analyst.md"
33
+ */
22
34
 
23
35
  // ── 入参($ARGS)──────────────────────────────────────────────────
24
36
  const target = $ARGS.target;
@@ -32,7 +44,21 @@ if (perspectives.some((p) => typeof p !== "string")) {
32
44
  throw new Error("parallel 参数 perspectives 必须是字符串数组,实际含非字符串元素");
33
45
  }
34
46
 
35
- log("parallel 开始,target=" + target + " perspectives=" + JSON.stringify(perspectives));
47
+ // S4:agents 参数 = 逗号分隔的 agentRef 路径数组;1 = 所有视角,N 个 = 一一对应
48
+ // worker 沙箱为 eval 模式:require 相对路径以 cwd 为基准(非脚本目录),
49
+ // 必须用 workerData.scriptPath 锚定脚本目录(review-fix-loop 同模式)。
50
+ const SCRIPT_DIR = workerData && workerData.scriptPath
51
+ ? require("path").dirname(workerData.scriptPath)
52
+ : process.cwd();
53
+ const { parseAgentRefs, agentRefAt } = require(SCRIPT_DIR + "/_shared/agent-refs.cjs");
54
+ const agentRefs = parseAgentRefs($ARGS.agents);
55
+ const agentFor = (i) => {
56
+ if (agentRefs.length === 1) return { agent: agentRefs[0] };
57
+ const ref = agentRefAt(agentRefs, i);
58
+ return ref ? { agent: ref } : {};
59
+ };
60
+
61
+ log("parallel 开始,target=" + target + " perspectives=" + JSON.stringify(perspectives) + (agentRefs.length ? " agents=" + agentRefs.join(",") : ""));
36
62
 
37
63
  let currentPhase = "init";
38
64
  let outcome;
@@ -44,7 +70,7 @@ try {
44
70
 
45
71
  // parallel() 接受 Promise 数组;agent() 返回 Promise。allSettled 语义。
46
72
  const perPerspectiveRaw = await parallel(
47
- perspectives.map((p) =>
73
+ perspectives.map((p, i) =>
48
74
  agent({
49
75
  prompt:
50
76
  "从「" + p + "」角度分析以下目标,给出评分和发现的问题:\n\n" + target,
@@ -62,6 +88,7 @@ try {
62
88
  required: ["perspective", "score", "findings"],
63
89
  },
64
90
  description: "parallel-" + p,
91
+ ...agentFor(i),
65
92
  })
66
93
  ),
67
94
  );
@@ -1,6 +1,6 @@
1
1
  // review-fix-loop-utils.cjs — review-fix-loop.js 的可测纯函数模块
2
2
  //
3
- // 与 recursive-split-utils.cjs 同款模式:workflow 编排逻辑的纯函数抽到独立 .cjs,
3
+ // workflow 编排逻辑的纯函数抽到独立 .cjs,
4
4
  // 供 vitest 单测直接 require(extensions/subagent-workflow/src/__tests__/review-fix-loop-utils.test.ts)
5
5
  // 与 worker 运行时共用(review-fix-loop.js 经 workerData.scriptPath 定位本文件)。
6
6
  //
@@ -8,35 +8,21 @@
8
8
  // 通过 fail(msg) 回调注入(调用方抛 "review-fix-loop: <msg>",与 workflow 内 fail() 一致)。
9
9
  "use strict";
10
10
 
11
- const fs = require("fs");
12
11
  const path = require("path");
13
12
 
14
13
  const TARGET_TYPES = ["git-diff", "file", "dir", "text"];
15
14
  const VALID_ARG_KEYS = new Set([
16
15
  "targetType", "target", "agents", "batchNames", "reviewPrompt", "fixPrompt",
17
- "autoCommit", "maxRounds", "stuckThreshold", "model", "skipCleanAgents",
18
- "recheckAfterFix", "fixAgent", "maxFixAttempts", "convergeNewIssues", "convergeRounds", "_runId",
16
+ "autoCommit", "maxRounds", "stuckThreshold", "skipCleanAgents",
17
+ "recheckAfterFix", "fixAgent", "maxFixAttempts", "convergeNewIssues", "convergeRounds",
18
+ "fallowScan", "_runId",
19
19
  ]);
20
20
 
21
- function normalizeBool(v, name, def, fail) {
22
- if (v === undefined || v === null || v === "") return def;
23
- if (v === true || v === "true") return true;
24
- if (v === false || v === "false") return false;
25
- fail("参数 " + name + " 必须是布尔值(true/false),实际: " + JSON.stringify(v));
26
- }
27
-
28
- function normalizeInt(v, name, def, fail) {
29
- if (v === undefined || v === null || v === "") return def;
30
- const n = typeof v === "number" ? v : Number(String(v).trim());
31
- if (!Number.isInteger(n) || n <= 0) fail("参数 " + name + " 必须是正整数,实际: " + JSON.stringify(v));
32
- return n;
33
- }
34
-
35
21
  /**
36
22
  * 批次解析:batch1..batchN(缺号报错)/ agents 简写。两者必传其一,缺省直接报错(无默认 agent)。
37
23
  * @param args $ARGS 形状的对象(batchN 键、agents 键)
38
24
  * @param fail 报错回调(抛错终止)
39
- * @returns string[][] 每批的 agent 名/文件路径数组
25
+ * @returns string[][] 每批的 agentRef 路径数组
40
26
  */
41
27
  function parseBatches(args, fail) {
42
28
  const batchKeys = Object.keys(args)
@@ -62,7 +48,7 @@ function parseBatches(args, fail) {
62
48
  return rawBatches.map((raw, idx) => {
63
49
  if (typeof raw !== "string" || !raw.trim()) fail("batch" + (idx + 1) + " 不能为空");
64
50
  const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
65
- if (names.length === 0) fail("batch" + (idx + 1) + " 为空(逗号分隔 agent 名/文件路径)");
51
+ if (names.length === 0) fail("batch" + (idx + 1) + " 为空(逗号分隔 agent .md 绝对路径)");
66
52
  if (new Set(names).size !== names.length) fail("batch" + (idx + 1) + " 内存在重复 agent: " + names);
67
53
  return names;
68
54
  });
@@ -76,15 +62,6 @@ function resolveBatchNames(rawBatchNames, batches, fail) {
76
62
  return rawBatchNames.length ? rawBatchNames : batches.map((_, i) => "batch-" + (i + 1));
77
63
  }
78
64
 
79
- /** fallow-scan 只在 git-diff 类型下有意义。 */
80
- function validateFallowScan(batches, targetType, fail) {
81
- for (let i = 0; i < batches.length; i++) {
82
- if (batches[i].includes("fallow-scan") && targetType !== "git-diff") {
83
- fail("fallow-scan 只支持 targetType=git-diff(它审查 git 变更的静态分析),实际 targetType=" + targetType);
84
- }
85
- }
86
- }
87
-
88
65
  /** 审查指令模板(按 targetType 生成,注入每个 review agent 的 prompt)。 */
89
66
  function buildReviewInstruction(targetType, target) {
90
67
  switch (targetType) {
@@ -694,17 +671,6 @@ function normalizeAggregatorResult(raw) {
694
671
  };
695
672
  }
696
673
 
697
- /** review- 前缀兜底判定:agent-registry.ts 的报错文案是 `Agent "${name}" not found. ...`
698
- * (名字夹在 "Agent" 与 "not found" 之间),不能用连续子串 "Agent not found" 匹配。
699
- * 已带 review- 前缀的 agent 不再重试(防死循环)。 */
700
- function shouldRetryWithReviewPrefix(error, agentName) {
701
- return typeof error === "string"
702
- && error.includes("not found")
703
- && typeof agentName === "string"
704
- && agentName.length > 0
705
- && !agentName.startsWith("review-");
706
- }
707
-
708
674
  /** 从 aggregated.md 内容回退解析(JSON 无效时的兜底,依赖 "- Must-fix: N" 固定格式)。 */
709
675
  function parseAggregatedMd(content) {
710
676
  const mustFixMatch = content.match(/[-*]\s*Must[-_]fix\s*[::]\s*(\d+)/i);
@@ -716,69 +682,29 @@ function parseAggregatedMd(content) {
716
682
  };
717
683
  }
718
684
 
719
- /**
720
- * 自定义 .md agent frontmatter 解析(纯函数,不碰 fs)。
721
- * 边界:无 `---` 头时 basename 兜底、frontmatter 未闭合(closeIdx === -1)截断、
722
- * 引号包裹的值剥引号、空值(`value || undefined`)回退。
723
- * @param content 文件原文
724
- * @param fallbackName 无 name 字段时的兜底(通常为 basename)
725
- * @returns {name, model, description, systemPrompt, report, title, isCustom}
726
- */
727
- function parseAgentMd(content, fallbackName) {
728
- let name = fallbackName;
729
- let model, description;
730
- let body = content.trim();
731
- if (content.startsWith("---")) {
732
- const closeIdx = content.indexOf("---", 3);
733
- if (closeIdx !== -1) {
734
- const yaml = content.slice(3, closeIdx);
735
- body = content.slice(closeIdx + 3).trim();
736
- const extract = (key) => {
737
- // 分隔符用 [ \t]* 而非 \s*:\s 含换行,空值 key(如 `name:`)后紧跟的下一行内容
738
- // 会被 \s* 吞掉换行后捕获成该 key 的值。仅匹配空格/制表符则空值 → .+ 不匹配 → undefined。
739
- const m = yaml.match(new RegExp("^" + key + ":[ \t]*(.+)$", "m"));
740
- if (!m) return undefined;
741
- let v = m[1].trim();
742
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
743
- return v || undefined;
744
- };
745
- name = extract("name") || name;
746
- model = extract("model");
747
- description = extract("description");
748
- }
749
- }
750
- return { name, model, description, systemPrompt: body, report: name, title: description || name, isCustom: true };
751
- }
752
-
753
- /** 自定义 .md agent 加载:fs 读取 + parseAgentMd。读取失败经 fail 回调(缺省时直接抛错)。 */
754
- function loadAgentMd(filePath, fail) {
755
- let content;
756
- try {
757
- content = fs.readFileSync(filePath, "utf-8");
758
- } catch (e) {
759
- const msg = "agent 文件读取失败: " + filePath + " (" + e.message + ")";
760
- if (typeof fail === "function") fail(msg);
761
- throw new Error("review-fix-loop: " + msg);
762
- }
763
- return parseAgentMd(content, path.basename(filePath, ".md"));
764
- }
765
-
766
- /** fallow-scan:内置工具型 def(无 .md,跑 fallow audit 静态分析)。 */
685
+ /** fallow-scan:内置工具型 def(无 .md,跑 fallow audit 静态分析)。
686
+ * 不由 batchN 触发(batchN 值域 = agent .md 路径)——由独立参数 fallowScan=true 在脚本层
687
+ * 前置插入为首批。 */
767
688
  const FALLOW_DEF = { name: "fallow-scan", title: "FALLOW STATIC ANALYSIS", report: "fallow-scan", isFallow: true };
768
689
 
769
690
  /**
770
- * Agent defs 解析:fallow-scan 常量 / 路径(含 `/` 或 `.md` 后缀)走 loader / 内置 agent 名。
771
- * 内置名做 `review-` 前缀剥离(与 runReviewAgent 的 review- 前缀兜底重试配对:
772
- * report 文件名用剥离后的名字,兜底重试才落到同一文件)。
773
- * @param batchNames 批内 agent 名/文件路径数组
774
- * @param loader 自定义 loader(测试注入 stub;缺省用 loadAgentMd)
691
+ * Agent defs 解析(S4 路径统一版):batchN/fixAgent 值全部是 agentRef(.md 绝对路径)。
692
+ *
693
+ * - def 只含标识(path/name/report/title),**不读文件**——agent 内容的加载与 systemPrompt
694
+ * 注入由主线程 resolveAgentOpts(agent-call path 加载)统一完成
695
+ * - `fallow-scan` 是脚本内部保留字(fallowScan 参数前置插入的首批),非用户参数值域
696
+ * @param batchNames 批内 agentRef 路径数组
775
697
  */
776
- function resolveAgentDefs(batchNames, loader) {
777
- const loadFn = loader || loadAgentMd;
698
+ function resolveAgentDefs(batchNames) {
778
699
  return batchNames.map((item) => {
779
700
  if (item === "fallow-scan") return FALLOW_DEF;
780
- if (item.includes("/") || item.endsWith(".md")) return loadFn(item);
781
- return { name: item, report: item.replace(/^review-/, ""), title: item.toUpperCase() };
701
+ if (!/^\/|^~\//.test(item) || !item.endsWith(".md")) {
702
+ throw new Error(
703
+ "review-fix-loop: 无效 agent 引用: " + item + "——必须是 .md 绝对路径(<available_subagents> 的 <location>)",
704
+ );
705
+ }
706
+ const name = item.split("/").pop().replace(/\.md$/, "");
707
+ return { path: item, name, report: name, title: name.toUpperCase() };
782
708
  });
783
709
  }
784
710
 
@@ -840,11 +766,8 @@ function resolveBatchTerminated(batchClean, terminated) {
840
766
  module.exports = {
841
767
  TARGET_TYPES,
842
768
  VALID_ARG_KEYS,
843
- normalizeBool,
844
- normalizeInt,
845
769
  parseBatches,
846
770
  resolveBatchNames,
847
- validateFallowScan,
848
771
  buildReviewInstruction,
849
772
  lockReviewBase,
850
773
  buildScopedRecheckPrompt,
@@ -865,9 +788,6 @@ module.exports = {
865
788
  parseResult,
866
789
  normalizeAggregatorResult,
867
790
  parseAggregatedMd,
868
- shouldRetryWithReviewPrefix,
869
- parseAgentMd,
870
- loadAgentMd,
871
791
  resolveAgentDefs,
872
792
  recordAgentClean,
873
793
  recordAgentDirty,
@@ -6,10 +6,12 @@
6
6
  //
7
7
  // 用法:
8
8
  // workflow run review-fix-loop --args targetType=git-diff target=main \
9
- // batch1=fallow-scan batch2=reviewer autoCommit=true
9
+ // batch1="/path/reviewer-a.md,/path/reviewer-b.md" autoCommit=true
10
10
  // workflow run review-fix-loop --args targetType=file target=/path/to/doc.md \
11
- // batch1=reviewer autoCommit=false
11
+ // batch1="/path/doc-reviewer.md" autoCommit=false
12
12
  //
13
+ // S4 路径统一:batch1..batchN/fixAgent 值 = agentRef(.md 绝对路径,<available_subagents> 的
14
+ // <location>);fallowScan=true 独立参数前置插入静态分析批次(不占 batchN)。
13
15
  // ⚠️ 唯一带写操作的内置 workflow:fix 阶段会修改文件(autoCommit=true 时 commit)。
14
16
  // ⚠️ lintScript 约束(本脚本已遵守):含 parallel() 入口,禁止 bare IIFE;
15
17
  // agent() 调用顺序确定(批次按配置顺序稳定排序,callId 重放安全)。
@@ -20,11 +22,45 @@
20
22
  // main 4.0.0 版自包含 677 行、缺批次参数时默认单批 ["reviewer"]、recheckAfterFix 默认 false。
21
23
  // merge 时保留本版(功能更全),详见 .changeset/tidy-waves-description-phase-lint.md。
22
24
 
23
- const meta = {
24
- name: "review-fix-loop",
25
- description: "审查-修复循环:多批串行(批内并行 review → aggregate → fix → 重审直到 clean)。必填 targetType(git-diff/file/dir/text)+ target。批次由必填参数 batch1..batchN 控制(无默认,至少传一个;agents 为单批简写;如 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
- };
25
+ /* @pi-meta
26
+ name: review-fix-loop
27
+ description: >-
28
+ 多批串行审查-修复循环:批内并行 review 聚合 must-fix 后迭代修复直到 clean
29
+ (唯一带写操作与 commit 副作用的内置 workflow,autoCommit 默认 false)
30
+ when: 用户要 review 并迭代修复至 clean
31
+ notFor: 单纯审查不改代码
32
+ phases: [Review, Fix]
33
+ parameters:
34
+ type: object
35
+ properties:
36
+ targetType: { type: string, enum: [git-diff, file, dir, text] }
37
+ target: { type: string, minLength: 1, pattern: '\S' }
38
+ autoCommit: { type: boolean, default: false }
39
+ maxRounds: { type: integer, default: 10, minimum: 1 }
40
+ stuckThreshold: { type: integer, default: 3, minimum: 1 }
41
+ skipCleanAgents: { type: boolean, default: true }
42
+ recheckAfterFix: { type: boolean, default: false }
43
+ fixAgent: { type: string }
44
+ maxFixAttempts: { type: integer, default: 2, minimum: 1 }
45
+ convergeNewIssues: { type: integer, default: 1, minimum: 1 }
46
+ convergeRounds: { type: integer, default: 2, minimum: 1 }
47
+ reviewPrompt: { type: string }
48
+ fixPrompt: { type: string }
49
+ fallowScan: { type: boolean, default: false }
50
+ agents: { type: string }
51
+ batchNames: { type: string }
52
+ patternProperties:
53
+ "^batch\\d+$":
54
+ type: string
55
+ description: 任意 batchN 编号,至少一个;值为 agent .md 绝对路径(逗号分隔多 agent)
56
+ required: [targetType, target]
57
+ usage: |
58
+ ## 使用说明
59
+ - batch1..batchN 与 agents 互斥(至少传一个 batchN);值 = agentRef(.md 绝对路径,<available_subagents> 的 <location>)
60
+ - fixAgent:fix 阶段执行者(agentRef),缺省用通用 subagent + 内联 fixPrompt
61
+ - fallowScan=true 仅 targetType=git-diff 合法(前置静态分析批次,不占 batchN)
62
+ - 示例:workflow run review-fix-loop --args targetType=git-diff target=main batch1="/path/fallow-agent.md,/path/reviewer.md" autoCommit=true
63
+ */
28
64
 
29
65
  // ── 参数解析 + 白名单校验(fail-fast) ────────────────────────────
30
66
 
@@ -33,19 +69,16 @@ function fail(msg) {
33
69
  }
34
70
 
35
71
  // ── 可测纯函数模块 ────────────────────────────────────────────────
36
- // 参数校验(normalizeBool/normalizeInt/白名单)/批次解析/聚合结果解析/审查指令构建
37
- // 的纯函数在 review-fix-loop-utils.cjs(与 recursive-split-utils.cjs 同款模式,
38
- // vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts)。
72
+ // 参数校验(白名单,类型校验由 m3 args-validator schema 接管)/批次解析/聚合结果解析/审查指令构建
73
+ // 的纯函数在 review-fix-loop-utils.cjs
74
+ // vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts
39
75
  // worker 运行时经 workerData.scriptPath 定位自身目录——内置 workflow 在 npm 包内,
40
76
  // process.cwd() 是用户项目目录,不能作为锚点;其他引擎无 workerData 时回退 cwd。
41
77
  const {
42
78
  TARGET_TYPES,
43
79
  VALID_ARG_KEYS,
44
- normalizeBool,
45
- normalizeInt,
46
80
  parseBatches,
47
81
  resolveBatchNames,
48
- validateFallowScan,
49
82
  buildReviewInstruction,
50
83
  lockReviewBase,
51
84
  buildScopedRecheckPrompt,
@@ -65,7 +98,6 @@ const {
65
98
  parseResult,
66
99
  normalizeAggregatorResult,
67
100
  parseAggregatedMd,
68
- shouldRetryWithReviewPrefix,
69
101
  resolveAgentDefs,
70
102
  recordAgentClean,
71
103
  recordAgentDirty,
@@ -82,7 +114,7 @@ const {
82
114
  for (const key of Object.keys($ARGS)) {
83
115
  if (VALID_ARG_KEYS.has(key)) continue;
84
116
  if (/^batch\d+$/.test(key)) continue;
85
- fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/model/skipCleanAgents/recheckAfterFix/fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds)");
117
+ fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/skipCleanAgents/recheckAfterFix/fallowScan/fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds)");
86
118
  }
87
119
 
88
120
  const targetType = $ARGS.targetType;
@@ -98,14 +130,26 @@ const reviewPrompt = typeof $ARGS.reviewPrompt === "string" && $ARGS.reviewPromp
98
130
  const fixPrompt = typeof $ARGS.fixPrompt === "string" && $ARGS.fixPrompt.trim()
99
131
  ? $ARGS.fixPrompt.trim()
100
132
  : "修复全部 must-fix 问题(critical/major)。最小正确修复,不做重构、不做风格改动。";
101
- const autoCommit = normalizeBool($ARGS.autoCommit, "autoCommit", false, fail);
102
- const maxRounds = normalizeInt($ARGS.maxRounds, "maxRounds", 10, fail);
103
- const stuckThreshold = normalizeInt($ARGS.stuckThreshold, "stuckThreshold", 3, fail);
104
- const skipCleanAgents = normalizeBool($ARGS.skipCleanAgents, "skipCleanAgents", true, fail);
133
+ // 字符串强制转换(m2 exec-review MAJOR-1):LLM 可能以字符串传布尔/整数(旧 normalizeBool
134
+ // 正是为此防御——"false" ?? false truthy 会误触发 commit)。m3 args-validator
135
+ // (chokepoint + coerceTypes)上线后此处为双保险,直接执行路径仍受保护。
136
+ const coerceBool = (v, fallback) =>
137
+ typeof v === "boolean" ? v : v === "true" ? true : v === "false" ? false : fallback;
138
+ const coerceInt = (v, fallback) =>
139
+ typeof v === "number" && Number.isInteger(v)
140
+ ? v
141
+ : typeof v === "string" && /^\d+$/.test(v.trim())
142
+ ? parseInt(v, 10)
143
+ : fallback;
144
+
145
+ const autoCommit = coerceBool($ARGS.autoCommit, false);
146
+ const maxRounds = coerceInt($ARGS.maxRounds, 10);
147
+ const stuckThreshold = coerceInt($ARGS.stuckThreshold, 3);
148
+ const skipCleanAgents = coerceBool($ARGS.skipCleanAgents, true);
105
149
  // 默认 recheckAfterFix=false:clean agent 下轮跳过(与 skipCleanAgents=true 字面语义一致),
106
150
  // RC-5(fix 后全批全量重审放大 token)在默认场景消失。传 true 启用可选强回归模式:fix 后重派
107
151
  // 全批,clean agent 走限定 prompt(buildScopedRecheckPrompt,只审 modifiedFiles,5.5)。
108
- const recheckAfterFix = normalizeBool($ARGS.recheckAfterFix, "recheckAfterFix", false, fail);
152
+ const recheckAfterFix = coerceBool($ARGS.recheckAfterFix, false);
109
153
  // fixAgent(5.3):值语义同 batchN 的 agent 项(内置名 / agent.md 路径),解析复用
110
154
  // resolveAgentDefs 白名单与加载逻辑。传入时 fix 阶段用 agent({agent: ...}) 派发(代码场景
111
155
  // 的 verify 命令写在该 agent.md 内);未传保持现状(通用 subagent + 内联 prompt)。
@@ -114,10 +158,10 @@ const FIX_AGENT_RAW = typeof $ARGS.fixAgent === "string" && $ARGS.fixAgent.trim(
114
158
  const FIX_DEF = FIX_AGENT_RAW ? resolveAgentDefs([FIX_AGENT_RAW])[0] : null;
115
159
  // 5.7 收敛终止参数:maxFixAttempts(needs-redesign 阈值,RC-7)/ convergeNewIssues +
116
160
  // convergeRounds(新发现率收敛阈值)
117
- const maxFixAttempts = normalizeInt($ARGS.maxFixAttempts, "maxFixAttempts", 2, fail);
118
- const convergeNewIssues = normalizeInt($ARGS.convergeNewIssues, "convergeNewIssues", 1, fail);
119
- const convergeRounds = normalizeInt($ARGS.convergeRounds, "convergeRounds", 2, fail);
120
- const MODEL = typeof $ARGS.model === "string" && $ARGS.model.trim() ? $ARGS.model.trim() : undefined;
161
+ const maxFixAttempts = coerceInt($ARGS.maxFixAttempts, 2);
162
+ const convergeNewIssues = coerceInt($ARGS.convergeNewIssues, 1);
163
+ const convergeRounds = coerceInt($ARGS.convergeRounds, 2);
164
+ const MODEL = $MODEL;
121
165
 
122
166
  // base 锁定(RC-6,5.6):git-diff 场景 run 启动时锁定 base commit,全程用锁定 hash 构造
123
167
  // diff 指令,防止 run 期间 base ref 被更新导致各轮 diff 范围不一致。rev-parse 失败(非 git
@@ -134,7 +178,15 @@ const reviewInstruction = buildReviewInstruction(targetType, lockedBase.base);
134
178
 
135
179
  // 批次解析:batch1..batchN(缺号报错)/ agents 简写;无默认批次——缺批次参数时
136
180
  // parseBatches 直接 fail-fast(与头注释「batch1..batchN/agents 必传」一致)
137
- const BATCHES = parseBatches($ARGS, fail);
181
+ const rawBatches = parseBatches($ARGS, fail);
182
+
183
+ // S4:fallowScan 独立参数(不由 batchN 触发——batchN 值域 = agentRef 路径)。
184
+ // fallow 作为内置首批前置插入(fallow-scan 保留字在 resolveAgentDefs 内解析)。
185
+ const fallowScan = coerceBool($ARGS.fallowScan, false);
186
+ if (fallowScan && targetType !== "git-diff") {
187
+ fail("fallowScan 只支持 targetType=git-diff(它审查 git 变更的静态分析),实际 targetType=" + targetType);
188
+ }
189
+ const BATCHES = fallowScan ? [["fallow-scan"], ...rawBatches] : rawBatches;
138
190
 
139
191
  // batchNames(数量校验)
140
192
  const rawBatchNames = typeof $ARGS.batchNames === "string" && $ARGS.batchNames.trim()
@@ -142,9 +194,6 @@ const rawBatchNames = typeof $ARGS.batchNames === "string" && $ARGS.batchNames.t
142
194
  : [];
143
195
  const BATCH_NAMES = resolveBatchNames(rawBatchNames, BATCHES, fail);
144
196
 
145
- // fallow-scan 只在 git-diff 类型下有意义
146
- validateFallowScan(BATCHES, targetType, fail);
147
-
148
197
  // ── Schemas ─────────────────────────────────────────────────────────
149
198
 
150
199
  const reviewerSchema = {
@@ -253,6 +302,32 @@ const STATE_FILE = RUN_ROOT + "/state.json";
253
302
  fs.mkdirSync(RUN_ROOT, { recursive: true });
254
303
  log("Run directory: " + RUN_ROOT);
255
304
 
305
+ // ── Startup fail-fast: validate agent ref paths exist (ADR-0003 D6) ─
306
+ // 启动期校验所有 batchN/fixAgent 路径存在,不存在立即报错(带 location 恢复指引),
307
+ // 避免跑到 round 中段 agent-call 时 loadByPath 失败才暴露。FALLOW_DEF(isFallow,
308
+ // 无 path)跳过——它是内置工具标记非文件路径。
309
+ function validateAgentPaths(defs) {
310
+ for (const def of defs) {
311
+ if (def.isFallow || !def.path) continue;
312
+ // MF-2:与 normalizeRef(src/shared/agent-ref.ts)对齐——~/ 前缀展开为 homedir 后再 statSync。
313
+ // resolveAgentDefs 接受 ~/ 前缀、normalizeRef 运行时也展开,此处不展开会「先接受后拒绝」误报 ENOENT。
314
+ const expanded = def.path.startsWith("~/")
315
+ ? path.join(os.homedir(), def.path.slice(2))
316
+ : def.path;
317
+ try {
318
+ fs.statSync(expanded);
319
+ } catch {
320
+ fail("Agent file not found: " + def.path + ". Check <available_subagents> <location> for valid agent refs (absolute .md path).");
321
+ }
322
+ }
323
+ }
324
+ const startupDefs = [];
325
+ for (const batch of BATCHES) {
326
+ startupDefs.push(...resolveAgentDefs(batch));
327
+ }
328
+ if (FIX_DEF) startupDefs.push(FIX_DEF);
329
+ validateAgentPaths(startupDefs);
330
+
256
331
  // ── State management (persistent, atomic writes) ────────────────────
257
332
 
258
333
  function loadState() {
@@ -310,16 +385,18 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
310
385
  ? "\nPrior batch reports (optional context): " + RUN_ROOT + "/batch-*/ (use read)"
311
386
  : "";
312
387
  const base = {
313
- model: MODEL || def.model,
388
+ model: MODEL,
314
389
  schema: reviewerSchema,
315
390
  description: def.name,
316
391
  timeoutMs: 3_600_000, // 1h(只读审查 + retry 退避余量)
317
- // returnMeta: true — recursive-split 脚本的 executeActionAgent 对齐:失败时 resolve
318
- // {value, error},raw.error 可检测(review- 前缀兜底/结构化终止可达);成功时
392
+ // returnMeta: true — 失败时 resolve
393
+ // {value, error},raw.error 可检测(结构化终止可达);成功时
319
394
  // value = parsedOutput ?? content,parseResult 作用于 raw.value(MF-1)。
320
395
  returnMeta: true,
321
396
  };
322
397
 
398
+ // S4:agentRef = 路径(非 fallow 的 def 必有 path)——systemPrompt/model 由主线程
399
+ // resolveAgentOpts 按 path 加载注入,脚本不再拼 md 内容。
323
400
  if (def.isFallow) {
324
401
  return {
325
402
  ...base,
@@ -345,9 +422,6 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
345
422
  // scoped 分支(recheck 限定)在下方单独处理(含对账段)。
346
423
  if (round > 1 && !scoped) {
347
424
  const prevRoundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1);
348
- const r2Spec = def.isCustom
349
- ? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt
350
- : "";
351
425
  return {
352
426
  ...base,
353
427
  schema: { ...reviewerSchema, required: [...reviewerSchema.required, "reconciliation"] },
@@ -359,8 +433,8 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
359
433
  ? state.fixResults[state.fixResults.length - 1]
360
434
  : null,
361
435
  knownRemaining: (state.knownRemaining && Array.isArray(state.knownRemaining)) ? state.knownRemaining : [],
362
- }) + r2Spec,
363
- agent: def.isCustom ? undefined : def.name,
436
+ }),
437
+ agent: def.path,
364
438
  };
365
439
  }
366
440
 
@@ -380,14 +454,11 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
380
454
  fixResult: state.fixResults && state.fixResults.length
381
455
  ? state.fixResults[state.fixResults.length - 1]
382
456
  : null,
383
- }) + (def.isCustom ? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt : ""),
384
- agent: def.isCustom ? undefined : def.name,
457
+ }),
458
+ agent: def.path,
385
459
  };
386
460
  }
387
461
 
388
- const spec = def.isCustom
389
- ? "\n\nReviewer specification (from agent file):\n" + def.systemPrompt
390
- : "";
391
462
  return {
392
463
  ...base,
393
464
  prompt: [
@@ -396,24 +467,18 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
396
467
  reviewInstruction + prevBatchesHint,
397
468
  "",
398
469
  "Review requirements:",
399
- reviewPrompt + spec,
470
+ reviewPrompt,
400
471
  "",
401
472
  "output 路径:" + roundDir + "/" + def.report + ".md",
402
473
  "Write report to: " + roundDir + "/" + def.report + ".md",
403
474
  ].join("\n"),
404
- agent: def.isCustom ? undefined : def.name,
475
+ agent: def.path,
405
476
  };
406
477
  }
407
478
 
408
- // agent 名解析失败(AgentRegistry not found,报错文案 `Agent "${name}" not found.`)时,尝试 review- 前缀兜底
479
+ // S4:agentRef = 路径,主线程按路径加载(systemPrompt 注入);无名字查找,无需前缀兜底
409
480
  async function runReviewAgent(call) {
410
- let raw = await agent(call);
411
- if (raw && typeof raw === "object" && raw.error
412
- && shouldRetryWithReviewPrefix(raw.error, call.agent)) {
413
- log("Agent not found: " + call.agent + " — retrying with review- prefix");
414
- raw = await agent({ ...call, agent: "review-" + call.agent });
415
- }
416
- return raw;
481
+ return agent(call);
417
482
  }
418
483
 
419
484
  // ── Main loop: batches (serial) × rounds (per-batch) ────────────────
@@ -777,21 +842,19 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
777
842
  prompt: buildFixPrompt({
778
843
  header: "Fix round " + round + " (batch " + batchIndex + ")",
779
844
  reportContent,
780
- fixPrompt: FIX_DEF && FIX_DEF.isCustom
781
- ? fixPrompt + "\n\nFixer specification (from agent file):\n" + FIX_DEF.systemPrompt
782
- : fixPrompt,
845
+ fixPrompt,
783
846
  commitInstr,
784
847
  caution: agg.fixes_caution && agg.fixes_caution.length ? agg.fixes_caution : [],
785
848
  }),
786
849
  schema: fixSchema,
787
- // buildReviewCall model: MODEL || def.model 对齐:custom fixer.md 的
788
- // frontmatter model 字段同样生效(之前丢弃了 FIX_DEF.model,只在 review 阶段消费)
789
- model: MODEL || (FIX_DEF && FIX_DEF.model),
850
+ // S4:fixAgent = agentRef 路径(主线程按路径加载 + frontmatter model 传播);
851
+ // 未传保持现状(通用 subagent + 内联 fixPrompt)。
852
+ model: MODEL,
790
853
  description: (FIX_DEF && FIX_DEF.name) || "fix",
791
854
  // fix 不设 timeoutMs = 不限时(execute-options-mapper: undefined/<=0 → 不设超时)。
792
855
  // 带写操作(改项目代码)可能很久(大重构/多文件),不应被墙钟超时打断。
793
856
  returnMeta: true,
794
- ...(FIX_DEF && !FIX_DEF.isCustom ? { agent: FIX_DEF.name } : {}),
857
+ ...(FIX_DEF && FIX_DEF.path ? { agent: FIX_DEF.path } : {}),
795
858
  });
796
859
 
797
860
  // returnMeta 下 fxRaw = {value, error}:先查 error(失败分支可达,MF-1),再对 value 做 parseResult
@@ -12,11 +12,22 @@
12
12
  //
13
13
  // ⚠️ lintScript 约束(本脚本已遵守):含 parallel() 入口(兼 agent 嵌套),禁止 bare IIFE
14
14
 
15
- const meta = {
16
- name: "scatter-gather",
17
- description: "通用编排:scatter 拆分 → parallel 处理 → gather 合并 三段",
18
- phases: ["scatter", "process", "gather"],
19
- };
15
+ /* @pi-meta
16
+ name: scatter-gather
17
+ description: 通用编排:scatter 拆分 → parallel 处理 → gather 合并 三段
18
+ phases: [scatter, process, gather]
19
+ parameters:
20
+ type: object
21
+ properties:
22
+ task: { type: string }
23
+ agents: { type: string }
24
+ required: [task]
25
+ usage: |
26
+ ## 使用说明
27
+ - scatter 拆解大任务 → process 并行处理 → gather 聚合结果
28
+ - agents:逗号分隔的 agent .md 绝对路径,按顺序对应 scatter/process/gather 三段
29
+ - 示例:workflow run scatter-gather --args task="<大任务描述>" agents="/path/splitter.md,/path/processor.md"
30
+ */
20
31
 
21
32
  // ── 入参($ARGS)──────────────────────────────────────────────────
22
33
  const task = $ARGS.task;
@@ -24,7 +35,17 @@ if (!task) {
24
35
  throw new Error("scatter-gather 缺少必需参数 task。用法:workflow run scatter-gather --args task=\"<大任务描述>\"");
25
36
  }
26
37
 
27
- log("scatter-gather 开始,task=" + task);
38
+ // S4:agents 参数 = 逗号分隔的 agentRef 路径数组,按顺序对应 scatter/process/gather
39
+ // worker 沙箱为 eval 模式:require 相对路径以 cwd 为基准(非脚本目录),
40
+ // 必须用 workerData.scriptPath 锚定脚本目录(review-fix-loop 同模式)。
41
+ const SCRIPT_DIR = workerData && workerData.scriptPath
42
+ ? require("path").dirname(workerData.scriptPath)
43
+ : process.cwd();
44
+ const { parseAgentRefs, agentRefAt } = require(SCRIPT_DIR + "/_shared/agent-refs.cjs");
45
+ const agentRefs = parseAgentRefs($ARGS.agents);
46
+ const stepAgent = (i) => { const ref = agentRefAt(agentRefs, i); return ref ? { agent: ref } : {}; };
47
+
48
+ log("scatter-gather 开始,task=" + task + (agentRefs.length ? ",agents=" + agentRefs.join(",") : ""));
28
49
 
29
50
  let currentPhase = "init";
30
51
  let outcome;
@@ -56,6 +77,7 @@ try {
56
77
  required: ["subtasks"],
57
78
  },
58
79
  description: "scatter-split",
80
+ ...stepAgent(0),
59
81
  });
60
82
 
61
83
  const subtasks = Array.isArray(split?.subtasks) ? split.subtasks : [];
@@ -84,6 +106,7 @@ try {
84
106
  required: ["subtask", "result"],
85
107
  },
86
108
  description: "scatter-process-" + s.name,
109
+ ...stepAgent(1),
87
110
  })
88
111
  ),
89
112
  );
@@ -129,6 +152,7 @@ try {
129
152
  required: ["mergedResult", "completeness"],
130
153
  },
131
154
  description: "scatter-gather-merge",
155
+ ...stepAgent(2),
132
156
  });
133
157
 
134
158
  outcome = {