ppxans-harness 2.4.0 → 3.2.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 (186) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +218 -265
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +15 -151
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +17 -10
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/brainstorm/SKILL.md +24 -0
  16. package/skills/cupid-lover-comms/SKILL.md +37 -0
  17. package/skills/debug/SKILL.md +26 -0
  18. package/skills/plan/SKILL.md +25 -0
  19. package/skills/ponytail/SKILL.md +25 -0
  20. package/skills/ppx-memory/SKILL.md +91 -0
  21. package/skills/ppx-memory/scripts/cli.js +192 -0
  22. package/skills/ppx-memory/scripts/experience.js +133 -0
  23. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  24. package/skills/ppx-memory/scripts/l0.js +52 -0
  25. package/skills/ppx-memory/scripts/l2.js +146 -0
  26. package/skills/ppx-memory/scripts/l3.js +112 -0
  27. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  28. package/skills/ppx-memory/scripts/pii.js +42 -0
  29. package/skills/ppx-memory/scripts/schema.js +80 -0
  30. package/skills/ppx-memory/scripts/session.js +398 -0
  31. package/skills/ppx-memory/scripts/similarity.js +43 -0
  32. package/skills/ppx-memory/scripts/store.js +116 -0
  33. package/skills/ppx-memory/scripts/wal.js +38 -0
  34. package/skills/ppx-selfheal/SKILL.md +24 -0
  35. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  36. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  37. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  38. package/skills/ppx-selfheal/scripts/store.js +116 -0
  39. package/skills/prompt-depth-kit/SKILL.md +28 -0
  40. package/skills/session-naming/SKILL.md +36 -0
  41. package/skills/verify/SKILL.md +25 -0
  42. package/src/agent/index.js +1340 -717
  43. package/src/agent/prompts.js +47 -3
  44. package/src/aml-server.js +197 -151
  45. package/src/ans/eviction.js +123 -143
  46. package/src/ans/guard.js +159 -120
  47. package/src/ans/lifecycle.js +96 -93
  48. package/src/ans/proactive.js +112 -129
  49. package/src/ans/reward.js +95 -111
  50. package/src/ans/values.js +15 -15
  51. package/src/audit/audit-chain.js +43 -7
  52. package/src/audit/verifier.js +157 -120
  53. package/src/bus/circuit-breaker.js +9 -1
  54. package/src/bus/runtime-bus.js +107 -93
  55. package/src/channels/base.js +57 -34
  56. package/src/channels/feishu.js +118 -126
  57. package/src/channels/http.js +1090 -592
  58. package/src/channels/index.js +111 -110
  59. package/src/channels/log.js +29 -29
  60. package/src/channels/wechat-crypto.js +73 -74
  61. package/src/channels/wechat.js +191 -197
  62. package/src/channels/workspace.js +94 -0
  63. package/src/channels-cli.js +126 -124
  64. package/src/cli.js +129 -120
  65. package/src/commands/index.js +142 -0
  66. package/src/config/channels.js +137 -170
  67. package/src/config/index.js +277 -224
  68. package/src/config/placeholder.js +31 -0
  69. package/src/config/providers.js +148 -188
  70. package/src/config/settings.js +156 -182
  71. package/src/core/policy.js +69 -18
  72. package/src/core/trace.js +8 -6
  73. package/src/edit/editblock.js +266 -0
  74. package/src/edit/snapshot.js +67 -0
  75. package/src/evidence/index.js +153 -0
  76. package/src/evolve/playbook.js +9 -10
  77. package/src/hooks/index.js +113 -0
  78. package/src/llm/client.js +188 -446
  79. package/src/llm/dsml.js +74 -74
  80. package/src/llm/embedder.js +41 -35
  81. package/src/llm/fence.js +52 -105
  82. package/src/llm/index.js +4 -4
  83. package/src/llm/local-embedder.js +94 -0
  84. package/src/llm/presets.js +113 -0
  85. package/src/llm/pricing.js +93 -0
  86. package/src/llm/retry.js +73 -73
  87. package/src/llm/router.js +92 -97
  88. package/src/mcp/admin.js +326 -0
  89. package/src/mcp/client.js +487 -375
  90. package/src/mcp/http.js +203 -0
  91. package/src/mcp/index.js +116 -116
  92. package/src/mcp/server.js +392 -0
  93. package/src/mcp/tasks.js +133 -0
  94. package/src/memory/asset-hub.js +6 -11
  95. package/src/memory/canvas.js +2 -5
  96. package/src/memory/compaction.js +28 -28
  97. package/src/memory/experience.js +133 -122
  98. package/src/memory/fact-store.js +914 -698
  99. package/src/memory/failure-episode.js +20 -11
  100. package/src/memory/fork.js +17 -8
  101. package/src/memory/index.js +8 -6
  102. package/src/memory/l0.js +53 -52
  103. package/src/memory/l2.js +145 -130
  104. package/src/memory/l3.js +111 -111
  105. package/src/memory/legion-board.js +71 -0
  106. package/src/memory/memory-ticker.js +239 -240
  107. package/src/memory/session.js +398 -397
  108. package/src/memory/sqlite-store.js +581 -0
  109. package/src/mode/blackboard.js +49 -49
  110. package/src/mode/graph.js +42 -41
  111. package/src/mode/index.js +64 -64
  112. package/src/mode/legion.js +54 -51
  113. package/src/mode/plan-exec.js +50 -50
  114. package/src/mode/router.js +28 -40
  115. package/src/orchestrator/agent-worker.js +69 -69
  116. package/src/orchestrator/dag.js +90 -83
  117. package/src/orchestrator/experts.js +76 -0
  118. package/src/orchestrator/index.js +1 -1
  119. package/src/orchestrator/legion.js +179 -187
  120. package/src/orchestrator/supervisor.js +6 -8
  121. package/src/permissions/index.js +378 -0
  122. package/src/persona/index.js +28 -29
  123. package/src/plugin/builtin.js +313 -212
  124. package/src/plugin/context.js +80 -79
  125. package/src/plugin/index.js +62 -62
  126. package/src/plugin/v3.js +73 -0
  127. package/src/protocol/index.js +148 -0
  128. package/src/repomap/index.js +309 -0
  129. package/src/review/index.js +393 -0
  130. package/src/seam/registry.js +3 -0
  131. package/src/seam/shell.js +55 -55
  132. package/src/security/injection.js +79 -0
  133. package/src/selfheal/evolve.js +67 -67
  134. package/src/selfheal/healer.js +184 -167
  135. package/src/selfheal/run.js +9 -9
  136. package/src/server.js +63 -60
  137. package/src/services/diagnose.js +180 -0
  138. package/src/services/learning-service.js +9 -0
  139. package/src/services/memory-health.js +34 -6
  140. package/src/services/memory-service.js +42 -9
  141. package/src/services/triage.js +138 -0
  142. package/src/session/parts.js +76 -0
  143. package/src/session/projection.js +73 -0
  144. package/src/session/rollout.js +54 -0
  145. package/src/session/turn.js +137 -0
  146. package/src/skills/lint.js +72 -0
  147. package/src/skills/loader.js +231 -150
  148. package/src/skills/search.js +58 -0
  149. package/src/skills/verify.js +95 -100
  150. package/src/tools/advanced.js +388 -352
  151. package/src/tools/builtin.js +384 -297
  152. package/src/tools/catalog.js +283 -159
  153. package/src/tools/command-guard.js +112 -112
  154. package/src/tools/custom.js +47 -47
  155. package/src/tools/delegate.js +383 -297
  156. package/src/tools/document.js +254 -253
  157. package/src/tools/git.js +151 -0
  158. package/src/tools/governance.js +47 -20
  159. package/src/tools/index.js +16 -11
  160. package/src/tools/methods.js +178 -178
  161. package/src/tools/ocr.js +59 -59
  162. package/src/tools/sandbox-worker.js +40 -0
  163. package/src/tools/sandbox.js +92 -0
  164. package/src/tools/seam.js +162 -125
  165. package/src/tools/selfmod.js +196 -176
  166. package/src/tools/v3.js +225 -0
  167. package/src/tools/vad.js +176 -0
  168. package/src/tools/voice.js +238 -0
  169. package/src/utils/async.js +14 -0
  170. package/src/utils/config-file.js +53 -0
  171. package/src/utils/crashguard.js +88 -0
  172. package/src/utils/http.js +53 -0
  173. package/src/utils/id.js +8 -0
  174. package/src/utils/json-state.js +33 -0
  175. package/src/utils/logger.js +17 -17
  176. package/src/utils/ndjson.js +25 -0
  177. package/src/utils/pii.js +42 -42
  178. package/src/utils/rate-limit.js +50 -0
  179. package/src/utils/schema.js +80 -0
  180. package/src/utils/similarity.js +43 -0
  181. package/src/utils/store.js +170 -108
  182. package/src/utils/text.js +15 -15
  183. package/src/utils/trace.js +153 -153
  184. package/src/utils/wal.js +39 -0
  185. package/src/utils/winutf8.js +16 -15
  186. package/src/wiki/index.js +170 -0
@@ -1,297 +1,383 @@
1
- // src/tools/delegate.js - 多 agent 自主协作工具 (spawn_agent)
2
- // 让 agent 在工具循环里自主决定 spawn 子 agent 分工 (ANS 神经元中枢的最小实现):
3
- // - 主 agent 分析任务 → 认为需要专门角色/并行/隔离 → 调 spawn_agent
4
- // - 子 agent: 独立会话目录 (隔离) + 共享全局经验库 (ANS 全局记忆)
5
- // - 子 agent 复用懒建军团 (agent._legion), 不重复 spawn 进程
6
- // v2 (2026-08-17): 吸收 Anthropic 多智能体研究洞察
7
- // - 并行任务: tasks 数组并行派发多个子 agent (专才 + 并行化)
8
- // - 差异化上下文: perspectives 注入每个子 agent 专属视角, 对抗同质失败
9
- // - 仲裁聚合: arbitrate 时主 agent LLM 综合各子结果做最终裁决
10
- // v3 (2026-08-17): 吸收 Superpowers SDD 子代理驱动开发
11
- // - review 循环: 实施者 -> 只读审查者 -> (发现问题 -> 修复 -> 复审) * fixRounds -> 熔断停放
12
- // - 审查者只读 (PPX_AGENT_READONLY), 实施者修复复用原进程 (上下文完整)
13
- // - 账本 ledger: 全程记录审查/修复轮次, 熔断时未决发现交主 agent 裁定
14
- import path from "node:path";
15
- import { Legion } from "../orchestrator/legion.js";
16
-
17
- const DELEGATE_TIMEOUT_MS = 120000; // 子任务最长等待 (防卡死主 agent 工具循环)
18
-
19
- // ---- SDD review 循环: 纯函数 (可测) ----
20
-
21
- // 严重级标签映射 (v1.0.8): 内部表示 Critical/Important/Minor, 展示用中文, 解析兼容中英
22
- export function severityLabel(s) {
23
- if (s === "Critical" || s === "严重" || s === "P0") return "严重";
24
- if (s === "Important" || s === "重要" || s === "P1") return "重要";
25
- return "次要";
26
- }
27
- function severityOf(token) {
28
- if (token === "严重" || token === "P0") return "Critical";
29
- if (token === "重要" || token === "P1") return "Important";
30
- if (token === "Critical" || token === "Important") return token; // 英文 token 原样映射
31
- return "Minor";
32
- }
33
-
34
- // 解析审查者输出 -> 发现列表 [{ severity, finding }]
35
- // 期望格式: 每行 "[严重|重要|次要] 描述" (兼容英文 Critical/Important/Minor), 无发现为 "(无发现)"
36
- export function parseReviewFindings(text) {
37
- if (!text) return [];
38
- const out = [];
39
- const re = /\[(严重|重要|次要|Critical|Important|Minor)\]\s*([^\n]+)/g;
40
- let m;
41
- while ((m = re.exec(String(text)))) {
42
- const finding = m[2].trim();
43
- if (finding) out.push({ severity: severityOf(m[1]), finding });
44
- }
45
- return out;
46
- }
47
-
48
- // 是否需要触发修复: 有 Critical/Important
49
- export function needsFix(findings) {
50
- return findings.some((f) => f.severity === "Critical" || f.severity === "Important");
51
- }
52
-
53
- // 组装审查者提示词 (只读审查契约)
54
- export function buildReviewPrompt(workDesc, judge, perspective) {
55
- const p = perspective ? `\n【审查视角】${perspective}` : "";
56
- return `你是只读审查者。审查下面"产出"中实施者的结果, 找出问题。严格遵守: 只读审查, 禁止修改/写入任何文件, 禁止执行命令。
57
-
58
- 【任务要求】${workDesc}
59
- 【审查准则】${judge || "对照任务要求检查: 功能正确性 / 需求满足度 / 边界情况 / 明显风险"}${p}
60
-
61
- 【产出】
62
- ${"<产出内容>"}
63
-
64
- 输出发现清单, 每行一条, 格式 "[严重级] 描述", 严重级用:
65
- - [严重] 功能错误 / 需求未满足 / 会导致失败
66
- - [重要] 质量缺陷 / 边界情况 / 明显风险
67
- - [次要] 小改进 / 风格
68
- 没有任何问题时只输出一行 "(无发现)"。不要输出其他内容。`;
69
- }
70
-
71
- // 修复提示词: 把未决发现交给实施者修复
72
- export function buildFixPrompt(task, findings) {
73
- const open = findings.filter((f) => f.severity === "Critical" || f.severity === "Important");
74
- const lines = open.map((f) => `[${severityLabel(f.severity)}] ${f.finding}`).join("\n");
75
- return `上一轮产出存在以下 ${open.length} 项问题, 请逐一修复 (只解决这些问题, 不要引入新问题):\n${lines}\n\n原始任务: ${task}`;
76
- }
77
-
78
- // ---- 纯函数: 组装多子结果 + 视角 (仲裁输入) ----
79
- export function buildArbitrationInput(tasks, results, perspectives) {
80
- return tasks.map((t, i) => {
81
- const p = perspectives?.[i] ? ` (视角: ${perspectives[i]})` : "";
82
- return `【子任务${i + 1}${p}】${t}\n【结果${i + 1}】${String(results[i] || "").slice(0, 2000)}`;
83
- }).join("\n\n");
84
- }
85
-
86
- // 主 agent 聚合评审 (仲裁者模式): 综合各子结果, 输出最终裁决
87
- // 无 LLM 或评审失败时退化为简单拼接 (不阻塞)
88
- export async function arbitrate(agent, tasks, results, perspectives, judge) {
89
- const input = buildArbitrationInput(tasks, results, perspectives);
90
- const system = "你是多 agent 结果的仲裁者。综合各方结果, 识别分歧与共识, 给出一个整合后的最终答案。直接输出最终答案, 不要复述过程。";
91
- const user = input + (judge ? `\n\n【评审要求】${judge}` : "");
92
- try {
93
- const r = await agent.llm.chat([
94
- { role: "system", content: system },
95
- { role: "user", content: user.slice(0, 6000) },
96
- ]);
97
- const text = String(r?.content || "").trim();
98
- return text || `(仲裁无输出)\n\n${input}`;
99
- } catch (e) {
100
- return `(仲裁失败, 直出各方结果)\n\n${input}`;
101
- }
102
- }
103
-
104
- // 带超时等待 (防子 agent 卡死) — 定时器必须清理, 否则快速 resolve 后仍挂起 120s 阻止进程退出
105
- function withTimeout(p, ms, label) {
106
- let timer;
107
- const timeout = new Promise((_, rej) => {
108
- timer = setTimeout(() => rej(new Error(`${label}超时 (${ms / 1000}s)`)), ms);
109
- });
110
- return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
111
- }
112
-
113
- // ---- SDD review 循环: 实施 -> 只读审查 -> (修复 -> 复审) * N -> 熔断 ----
114
- // 返回: 通过时 "✅ 审查通过..." + 产出; 熔断时 "⚠️ 未决发现停放..." + 产出
115
- // namePrefix: 多任务 review 时传入 `${role}_${ts}_${i}`, 保证每对 agent 名唯一
116
- async function runReviewLoop({ agent, L, task, perspective, role, judge, fixRounds, namePrefix = null }) {
117
- const ts = Date.now().toString(36);
118
- const implName = namePrefix ? `${namePrefix}_impl` : `${role}_impl_${ts}`;
119
- const revName = namePrefix ? `${namePrefix}_rev` : `${role}_rev_${ts}`;
120
- const mkOpts = (n) => ({ dataDir: path.join(agent.dataDir, "legion", n), globalDataDir: agent.globalDataDir });
121
- L.spawnAgent(implName, mkOpts(implName));
122
- // 审查者只读: PPX_AGENT_READONLY=1 时 worker 禁用全部修改/执行工具
123
- L.spawnAgent(revName, { ...mkOpts(revName), env: { PPX_AGENT_READONLY: "1" } });
124
- if (agent.lifecycle) agent.lifecycle.reproduce(2);
125
-
126
- const max = (() => { // v1.0.8: fix_rounds=0 应能设 0 (原 `|| 3` 把 0 变 3)
127
- const parsed = Number(fixRounds);
128
- return Number.isFinite(parsed) ? Math.min(Math.max(parsed, 0), 5) : 3;
129
- })(); // 熔断上限 (Superpowers 5 轮, 默认 3 控成本; 0 = 不修复直接停放)
130
- const ledger = [];
131
- let findings = [];
132
- let result = "";
133
-
134
- // 1. 实施
135
- try {
136
- const r = await withTimeout(L.send(implName, { type: "chat", message: task, perspective }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }), DELEGATE_TIMEOUT_MS, "实施");
137
- result = String(r?.reply || "").trim() || "(实施者无回复)";
138
- } catch (e) {
139
- return `[工具错误] spawn_agent(review): 实施失败: ${e.message}`;
140
- }
141
- ledger.push({ round: 0, step: "implement" });
142
-
143
- // 2. 审查 + 修复循环
144
- let round = 0;
145
- while (true) {
146
- const reviewText = await (async () => {
147
- try {
148
- const r = await withTimeout(L.send(revName, { type: "chat", message: buildReviewPrompt(task, judge, perspective) + `\n\n【产出】\n${result.slice(0, 6000)}`, perspective }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }), DELEGATE_TIMEOUT_MS, "审查");
149
- return String(r?.reply || "");
150
- } catch (e) {
151
- return `[Critical] 审查者不可用: ${e.message}`;
152
- }
153
- })();
154
- findings = parseReviewFindings(reviewText);
155
- ledger.push({ round, step: "review", findings });
156
- if (!needsFix(findings)) break; // 通过
157
- if (round >= max) break; // 熔断
158
- round++;
159
- try {
160
- const r = await withTimeout(L.send(implName, { type: "chat", message: buildFixPrompt(task, findings), perspective }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }), DELEGATE_TIMEOUT_MS, `修复第${round}轮`);
161
- result = String(r?.reply || "").trim() || "(实施者无回复)";
162
- } catch (e) {
163
- ledger.push({ round, step: "fix", error: e.message });
164
- break;
165
- }
166
- ledger.push({ round, step: "fix" });
167
- }
168
-
169
- // 3. 汇总: 通过 or 熔断停放 (账本交主 agent 裁定)
170
- const open = findings.filter((f) => f.severity === "Critical" || f.severity === "Important");
171
- if (open.length) {
172
- return `⚠️ 审查未通过: 达到修复上限 (${max} 轮), 以下 ${open.length} 项未决发现已停放, 请主 agent 裁定是否接受当前产出:\n`
173
- + open.map((f) => `- [${severityLabel(f.severity)}] ${f.finding}`).join("\n")
174
- + `\n\n当前产出:\n${result}`;
175
- }
176
- const summary = findings.length
177
- ? findings.map((f) => `[${severityLabel(f.severity)}]`).join(" ")
178
- : "无";
179
- return `✅ 审查通过 (审查发现: ${summary})\n\n${result}`;
180
- }
181
-
182
- export function registerDelegateTools(catalog, _opts = {}) {
183
- catalog.register({
184
- name: "spawn_agent",
185
- description: "派生子 agent 处理子任务并等待结果。适合需要专门角色、并行、或隔离执行的任务 (如数据分析、代码审查、多角度论证)。子 agent 共享全局经验库。支持: 单个 task; 或 tasks 数组并行派发多个子 agent + perspectives 差异化视角; arbitrate=true 时主 agent 仲裁聚合各方结果; review=true 时走 SDD 审查循环: 实施者干活 -> 只读审查者挑问题 -> 修复 -> 复审, 达上限熔断停放交主 agent 裁定 (单任务直接审查; 多任务每个子任务独立一对实施+审查, 可配 arbitrate 聚合)。",
186
- parameters: {
187
- type: "object",
188
- properties: {
189
- task: { type: "string", description: "单个子任务描述 (清晰完整, 含上下文); 与 tasks 二选一" },
190
- tasks: { type: "array", items: { type: "string" }, description: "并行子任务列表 (每个子 agent 一个), 适合多角度论证/并行处理; 与 task 二选一" },
191
- perspectives: { type: "array", items: { type: "string" }, description: "差异化视角列表, 与 tasks 一一对应, 注入每个子 agent 专属视角 (对抗同质失败), 可缺省" },
192
- role: { type: "string", description: "子 agent 角色名 (如 数据分析师/代码审查员), 默认 helper" },
193
- arbitrate: { type: "boolean", description: "是否由主 agent 仲裁聚合所有子结果 (并行/多任务 review 时推荐), 默认 false 直接返回拼接结果" },
194
- judge: { type: "string", description: "仲裁评审指令 (arbitrate=true 时生效, 如 找出最可靠结论/合并去重); review=true 时为审查准则, 可缺省" },
195
- review: { type: "boolean", description: "SDD 审查循环: 实施者 -> 只读审查者 -> 发现问题自动修复复审, 达上限熔断, 默认 false。单 task 与多 tasks 均支持" },
196
- fix_rounds: { type: "number", description: "审查循环最大修复轮数 (review=true 时生效, 默认 3, 上限 5)" },
197
- },
198
- },
199
- execute: async (args, ctx) => {
200
- const agent = ctx?.agent;
201
- if (!agent) return "[工具错误] spawn_agent: 无 agent 上下文";
202
- // 先校验参数, 再校验环境 (输入校验优先)
203
- let tasks = null;
204
- if (Array.isArray(args.tasks) && args.tasks.length) {
205
- tasks = args.tasks.map((t) => String(t).slice(0, 4000));
206
- } else if (args.task) {
207
- tasks = [String(args.task).slice(0, 4000)];
208
- }
209
- if (!tasks) return "[工具错误] spawn_agent: 需要 task 或 tasks";
210
- if (!agent.llm) return "[工具错误] spawn_agent: 主 agent 未配置模型, 无法委派";
211
- // 懒建军团 (复用已有, 避免重复 spawn 进程)
212
- let L = agent._legion;
213
- if (!L) { L = new Legion(); agent._legion = L; }
214
- const role = String(args.role || "helper").replace(/[^\w-]/g, "_").slice(0, 24);
215
-
216
- const perspectives = Array.isArray(args.perspectives) ? args.perspectives.map((p) => String(p)).slice(0, tasks.length) : [];
217
-
218
- try {
219
- // SDD review 循环: 实施 -> 审查 -> 修复 -> 熔断
220
- // 单任务: 直接跑; 多任务: 每个任务独立一对 (实施者+只读审查者), 并行跑, 可仲裁聚合
221
- if (args.review) {
222
- const prefix = `${role}_${Date.now().toString(36)}`;
223
- if (tasks.length === 1) {
224
- return await runReviewLoop({
225
- agent, L, task: tasks[0],
226
- perspective: perspectives[0],
227
- role, judge: args.judge,
228
- fixRounds: args.fix_rounds,
229
- namePrefix: `${prefix}_0`,
230
- });
231
- }
232
- // 多任务: 并行各任务 review, 各自独立 (agent 名唯一, 不冲突)
233
- const settled = await Promise.all(tasks.map(async (task, i) => {
234
- try {
235
- return await runReviewLoop({
236
- agent, L, task,
237
- perspective: perspectives[i],
238
- role, judge: args.judge,
239
- fixRounds: args.fix_rounds,
240
- namePrefix: `${prefix}_${i}`,
241
- });
242
- } catch (e) {
243
- return `[子任务${i + 1} review 失败] ${e.message}`;
244
- }
245
- }));
246
- if (args.arbitrate) {
247
- return await arbitrate(agent, tasks, settled, perspectives, args.judge);
248
- }
249
- return tasks.map((t, i) => {
250
- const p = perspectives?.[i] ? ` (${perspectives[i]})` : "";
251
- return `【子任务${i + 1}${p}】${t}\n${settled[i]}`;
252
- }).join("\n\n");
253
- }
254
- // 并行 spawn 子 agent: 每个独立数据目录 + 独立视角
255
- const names = tasks.map((_, i) => `${role}_${i}_${Date.now().toString(36)}`);
256
- for (const n of names) {
257
- L.spawnAgent(n, {
258
- dataDir: path.join(agent.dataDir, "legion", n),
259
- globalDataDir: agent.globalDataDir,
260
- });
261
- }
262
- // 生命周期: 繁衍计数 (ANS: reproducing)
263
- if (agent.lifecycle) agent.lifecycle.reproduce(names.length);
264
-
265
- // 并行派发, 全部等结果 (各自独立超时)
266
- const settled = await Promise.all(tasks.map(async (task, i) => {
267
- try {
268
- const reply = await withTimeout(
269
- L.send(names[i], { type: "chat", message: task, perspective: perspectives[i] }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }),
270
- DELEGATE_TIMEOUT_MS,
271
- `子任务${i + 1}`
272
- );
273
- return { ok: true, reply: reply.reply || "(子 agent 无回复)" };
274
- } catch (e) {
275
- return { ok: false, reply: `[子任务${i + 1}失败] ${e.message}` };
276
- }
277
- }));
278
- const results = settled.map((s) => s.reply);
279
-
280
- // 单任务: 保持旧行为, 直接返回子 agent 回复
281
- if (tasks.length === 1) return results[0];
282
-
283
- // 多任务: 有 arbitrate 走主 agent 仲裁聚合, 否则拼接各方结果
284
- if (args.arbitrate) {
285
- const out = await arbitrate(agent, tasks, results, perspectives, args.judge);
286
- return out;
287
- }
288
- return tasks.map((t, i) => {
289
- const p = perspectives?.[i] ? ` (${perspectives[i]})` : "";
290
- return `【子任务${i + 1}${p}】${t}\n${results[i]}`;
291
- }).join("\n\n");
292
- } catch (e) {
293
- return `[工具错误] spawn_agent: ${e.message}`;
294
- }
295
- },
296
- });
297
- }
1
+ // src/tools/delegate.js - 多 agent 自主协作工具 (spawn_agent)
2
+ // 让 agent 在工具循环里自主决定 spawn 子 agent 分工 (ANS 神经元中枢的最小实现):
3
+ // - 主 agent 分析任务 → 认为需要专门角色/并行/隔离 → 调 spawn_agent
4
+ // - 子 agent: 独立会话目录 (隔离) + 共享全局经验库 (ANS 全局记忆)
5
+ // - 子 agent 复用懒建军团 (agent._legion), 不重复 spawn 进程
6
+ // v2 (2026-08-17): 吸收 Anthropic 多智能体研究洞察
7
+ // - 并行任务: tasks 数组并行派发多个子 agent (专才 + 并行化)
8
+ // - 差异化上下文: perspectives 注入每个子 agent 专属视角, 对抗同质失败
9
+ // - 仲裁聚合: arbitrate 时主 agent LLM 综合各子结果做最终裁决
10
+ // v3 (2026-08-17): 吸收 Superpowers SDD 子代理驱动开发
11
+ // - review 循环: 实施者 -> 只读审查者 -> (发现问题 -> 修复 -> 复审) * fixRounds -> 熔断停放
12
+ // - 审查者只读 (PPX_AGENT_READONLY), 实施者修复复用原进程 (上下文完整)
13
+ // - 账本 ledger: 全程记录审查/修复轮次, 熔断时未决发现交主 agent 裁定
14
+ import path from "node:path";
15
+ import { Legion } from "../orchestrator/legion.js";
16
+ import { runSupervisor } from "../orchestrator/supervisor.js"; // 2026-10-03 接线
17
+ import { resolveExpert, listExperts } from "../orchestrator/experts.js";
18
+ import { withTimeout } from "../utils/async.js";
19
+
20
+ const DELEGATE_TIMEOUT_MS = 120000; // 子任务最长等待 (防卡死主 agent 工具循环)
21
+
22
+ // ---- SDD review 循环: 纯函数 (可测) ----
23
+
24
+ // 严重级标签映射 (v1.0.8): 内部表示 Critical/Important/Minor, 展示用中文, 解析兼容中英
25
+ export function severityLabel(s) {
26
+ if (s === "Critical" || s === "严重" || s === "P0") return "严重";
27
+ if (s === "Important" || s === "重要" || s === "P1") return "重要";
28
+ return "次要";
29
+ }
30
+ function severityOf(token) {
31
+ if (token === "严重" || token === "P0") return "Critical";
32
+ if (token === "重要" || token === "P1") return "Important";
33
+ if (token === "Critical" || token === "Important") return token; // 英文 token 原样映射
34
+ return "Minor";
35
+ }
36
+
37
+ // 解析审查者输出 -> 发现列表 [{ severity, finding }]
38
+ // 期望格式: 每行 "[严重|重要|次要] 描述" (兼容英文 Critical/Important/Minor), 无发现为 "(无发现)"
39
+ export function parseReviewFindings(text) {
40
+ if (!text) return [];
41
+ const out = [];
42
+ const re = /\[(严重|重要|次要|Critical|Important|Minor)\]\s*([^\n]+)/g;
43
+ let m;
44
+ while ((m = re.exec(String(text)))) {
45
+ const finding = m[2].trim();
46
+ if (finding) out.push({ severity: severityOf(m[1]), finding });
47
+ }
48
+ return out;
49
+ }
50
+
51
+ // 是否需要触发修复: 有 Critical/Important
52
+ export function needsFix(findings) {
53
+ return findings.some((f) => f.severity === "Critical" || f.severity === "Important");
54
+ }
55
+
56
+ // 组装审查者提示词 (只读审查契约)
57
+ export function buildReviewPrompt(workDesc, judge, perspective) {
58
+ const p = perspective ? `\n【审查视角】${perspective}` : "";
59
+ return `你是只读审查者。审查下面"产出"中实施者的结果, 找出问题。严格遵守: 只读审查, 禁止修改/写入任何文件, 禁止执行命令。
60
+
61
+ 【任务要求】${workDesc}
62
+ 【审查准则】${judge || "对照任务要求检查: 功能正确性 / 需求满足度 / 边界情况 / 明显风险"}${p}
63
+
64
+ 【产出】
65
+ ${"<产出内容>"}
66
+
67
+ 输出发现清单, 每行一条, 格式 "[严重级] 描述", 严重级用:
68
+ - [严重] 功能错误 / 需求未满足 / 会导致失败
69
+ - [重要] 质量缺陷 / 边界情况 / 明显风险
70
+ - [次要] 小改进 / 风格
71
+ 没有任何问题时只输出一行 "(无发现)"。不要输出其他内容。`;
72
+ }
73
+
74
+ // 修复提示词: 把未决发现交给实施者修复
75
+ export function buildFixPrompt(task, findings) {
76
+ const open = findings.filter((f) => f.severity === "Critical" || f.severity === "Important");
77
+ const lines = open.map((f) => `[${severityLabel(f.severity)}] ${f.finding}`).join("\n");
78
+ return `上一轮产出存在以下 ${open.length} 项问题, 请逐一修复 (只解决这些问题, 不要引入新问题):\n${lines}\n\n原始任务: ${task}`;
79
+ }
80
+
81
+ // ---- 纯函数: 组装多子结果 + 视角 (仲裁输入) ----
82
+ export function buildArbitrationInput(tasks, results, perspectives) {
83
+ return tasks.map((t, i) => {
84
+ const p = perspectives?.[i] ? ` (视角: ${perspectives[i]})` : "";
85
+ return `【子任务${i + 1}${p}】${t}\n【结果${i + 1}】${String(results[i] || "").slice(0, 2000)}`;
86
+ }).join("\n\n");
87
+ }
88
+
89
+ // 主 agent 聚合评审 (仲裁者模式): 综合各子结果, 输出最终裁决
90
+ // 无 LLM 或评审失败时退化为简单拼接 (不阻塞)
91
+ export async function arbitrate(agent, tasks, results, perspectives, judge) {
92
+ const input = buildArbitrationInput(tasks, results, perspectives);
93
+ const system = "你是多 agent 结果的仲裁者。综合各方结果, 识别分歧与共识, 给出一个整合后的最终答案。直接输出最终答案, 不要复述过程。";
94
+ const user = input + (judge ? `\n\n【评审要求】${judge}` : "");
95
+ try {
96
+ const r = await agent.llm.chat([
97
+ { role: "system", content: system },
98
+ { role: "user", content: user.slice(0, 6000) },
99
+ ]);
100
+ const text = String(r?.content || "").trim();
101
+ return text || `(仲裁无输出)\n\n${input}`;
102
+ } catch (e) {
103
+ return `(仲裁失败, 直出各方结果)\n\n${input}`;
104
+ }
105
+ }
106
+
107
+ // (withTimeout 收敛到 utils/async.js: 原先本文件与 orchestrator/supervisor.js 各写一份)
108
+
109
+ // 仲裁 + 记忆板上下文 (2026-10-02): share_board 时先读板 (本角色最近发布), 让仲裁者看到军团累积知识。
110
+ // 板为空/异常时静默退化为纯仲裁, 不阻塞。
111
+ export async function arbitrateWithBoard(agent, tasks, results, perspectives, judge, { board, shareBoard, boardTopic } = {}) {
112
+ let boardContext = "";
113
+ if (shareBoard && board) {
114
+ try {
115
+ const entries = board.query({ topic: boardTopic, limit: 20 });
116
+ if (entries.length) {
117
+ boardContext = "\n\n【军团记忆板 (本角色近期发布)】\n" + entries.map((e) => `- ${e.from}: ${e.content}`).join("\n");
118
+ }
119
+ } catch { /* 读板失败不阻塞仲裁 */ }
120
+ }
121
+ return arbitrate(agent, tasks, results, perspectives, judge + boardContext);
122
+ }
123
+
124
+ // ---- SDD review 循环: 实施 -> 只读审查 -> (修复 -> 复审) * N -> 熔断 ----
125
+ // 返回: 通过时 "✅ 审查通过..." + 产出; 熔断时 "⚠️ 未决发现停放..." + 产出
126
+ // namePrefix: 多任务 review 时传入 `${role}_${ts}_${i}`, 保证每对 agent 名唯一
127
+ async function runReviewLoop({ agent, L, task, perspective, role, judge, fixRounds, namePrefix = null }) {
128
+ const ts = Date.now().toString(36);
129
+ const implName = namePrefix ? `${namePrefix}_impl` : `${role}_impl_${ts}`;
130
+ const revName = namePrefix ? `${namePrefix}_rev` : `${role}_rev_${ts}`;
131
+ const mkOpts = (n) => ({ dataDir: path.join(agent.dataDir, "legion", n), globalDataDir: agent.globalDataDir });
132
+ L.spawnAgent(implName, mkOpts(implName));
133
+ // 审查者只读: PPX_AGENT_READONLY=1 时 worker 禁用全部修改/执行工具
134
+ L.spawnAgent(revName, { ...mkOpts(revName), env: { PPX_AGENT_READONLY: "1" } });
135
+ if (agent.lifecycle) agent.lifecycle.reproduce(2);
136
+
137
+ const max = (() => { // v1.0.8: fix_rounds=0 应能设 0 (原 `|| 3` 把 0 变 3)
138
+ const parsed = Number(fixRounds);
139
+ return Number.isFinite(parsed) ? Math.min(Math.max(parsed, 0), 5) : 3;
140
+ })(); // 熔断上限 (Superpowers 5 轮, 默认 3 控成本; 0 = 不修复直接停放)
141
+ const ledger = [];
142
+ let findings = [];
143
+ let result = "";
144
+
145
+ // 1. 实施
146
+ try {
147
+ const r = await withTimeout(L.send(implName, { type: "chat", message: task, perspective }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }), DELEGATE_TIMEOUT_MS, "实施");
148
+ result = String(r?.reply || "").trim() || "(实施者无回复)";
149
+ } catch (e) {
150
+ return `[工具错误] spawn_agent(review): 实施失败: ${e.message}`;
151
+ }
152
+ ledger.push({ round: 0, step: "implement" });
153
+
154
+ // 2. 审查 + 修复循环
155
+ let round = 0;
156
+ while (true) {
157
+ const reviewText = await (async () => {
158
+ try {
159
+ const r = await withTimeout(L.send(revName, { type: "chat", message: buildReviewPrompt(task, judge, perspective) + `\n\n【产出】\n${result.slice(0, 6000)}`, perspective }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }), DELEGATE_TIMEOUT_MS, "审查");
160
+ return String(r?.reply || "");
161
+ } catch (e) {
162
+ return `[Critical] 审查者不可用: ${e.message}`;
163
+ }
164
+ })();
165
+ findings = parseReviewFindings(reviewText);
166
+ ledger.push({ round, step: "review", findings });
167
+ if (!needsFix(findings)) break; // 通过
168
+ if (round >= max) break; // 熔断
169
+ round++;
170
+ try {
171
+ const r = await withTimeout(L.send(implName, { type: "chat", message: buildFixPrompt(task, findings), perspective }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }), DELEGATE_TIMEOUT_MS, `修复第${round}轮`);
172
+ result = String(r?.reply || "").trim() || "(实施者无回复)";
173
+ } catch (e) {
174
+ ledger.push({ round, step: "fix", error: e.message });
175
+ break;
176
+ }
177
+ ledger.push({ round, step: "fix" });
178
+ }
179
+
180
+ // 3. 汇总: 通过 or 熔断停放 (账本交主 agent 裁定)
181
+ const open = findings.filter((f) => f.severity === "Critical" || f.severity === "Important");
182
+ if (open.length) {
183
+ return `⚠️ 审查未通过: 达到修复上限 (${max} 轮), 以下 ${open.length} 项未决发现已停放, 请主 agent 裁定是否接受当前产出:\n`
184
+ + open.map((f) => `- [${severityLabel(f.severity)}] ${f.finding}`).join("\n")
185
+ + `\n\n当前产出:\n${result}`;
186
+ }
187
+ const summary = findings.length
188
+ ? findings.map((f) => `[${severityLabel(f.severity)}]`).join(" ")
189
+ : "无";
190
+ return `✅ 审查通过 (审查发现: ${summary})\n\n${result}`;
191
+ }
192
+
193
+ // ---- 军团共享记忆板接入 (2026-10-02) ----
194
+ // 子任务结论自动发布到记忆板 (topic=角色), 仲裁前自动读板注入。
195
+ // 发布永不阻塞委派: 任何异常吞掉 (记忆板是增强, 不是依赖)。
196
+ export function publishToBoard(board, { from, topic, task, reply, status = "完成" }) {
197
+ if (!board) return;
198
+ try {
199
+ board.publish({
200
+ from,
201
+ topic,
202
+ content: `[${status}] 任务: ${String(task).slice(0, 120)} → 结论: ${String(reply).slice(0, 400)}`,
203
+ });
204
+ } catch { /* 记忆板满/IO 异常均不阻塞委派 */ }
205
+ }
206
+
207
+ export function registerDelegateTools(catalog, opts = {}) {
208
+ const board = opts.board || null;
209
+ catalog.register({
210
+ name: "spawn_agent",
211
+ description: "派生子 agent 处理子任务并等待结果。适合需要专门角色、并行、或隔离执行的任务 (如数据分析、代码审查、多角度论证)。子 agent 共享全局经验库。支持: 单个 task; 或 tasks 数组并行派发多个子 agent + perspectives 差异化视角; arbitrate=true 时主 agent 仲裁聚合各方结果; review=true 时走 SDD 审查循环: 实施者干活 -> 只读审查者挑问题 -> 修复 -> 复审, 达上限熔断停放交主 agent 裁定 (单任务直接审查; 多任务每个子任务独立一对实施+审查, 可配 arbitrate 聚合)。",
212
+ parameters: {
213
+ type: "object",
214
+ properties: {
215
+ task: { type: "string", description: "单个子任务描述 (清晰完整, 含上下文); 与 tasks 二选一" },
216
+ tasks: { type: "array", items: { type: "string" }, description: "并行子任务列表 (每个子 agent 一个), 适合多角度论证/并行处理; 与 task 二选一" },
217
+ perspectives: { type: "array", items: { type: "string" }, description: "差异化视角列表, 与 tasks 一一对应, 注入每个子 agent 专属视角 (对抗同质失败), 可缺省" },
218
+ role: { type: "string", description: "子 agent 角色名 (如 数据分析师/代码审查员), 默认 helper" },
219
+ arbitrate: { type: "boolean", description: "是否由主 agent 仲裁聚合所有子结果 (并行/多任务 review 时推荐), 默认 false 直接返回拼接结果" },
220
+ judge: { type: "string", description: "仲裁评审指令 (arbitrate=true 时生效, 如 找出最可靠结论/合并去重); review=true 时为审查准则, 可缺省" },
221
+ review: { type: "boolean", description: "SDD 审查循环: 实施者 -> 只读审查者 -> 发现问题自动修复复审, 达上限熔断, 默认 false。单 task 与多 tasks 均支持" },
222
+ fix_rounds: { type: "number", description: "审查循环最大修复轮数 (review=true 时生效, 默认 3, 上限 5)" },
223
+ share_board: { type: "boolean", description: "子任务结论自动发布到军团共享记忆板 (board_query 可查), 仲裁前主 agent 自动读板。默认 true" },
224
+ expert: { type: "string", description: `固化专家角色 (单任务)。名册: ${listExperts()}。专家自带角色名+专属视角 (只读专家自动禁修改工具)` },
225
+ experts: { type: "array", items: { type: "string" }, description: "每任务一个专家 (与 tasks 一一对应, 优先于 expert)。如 [\"code\",\"design\",\"security\"] 三任务分派代码/设计/安全专家; supervisor=true 时为编排专家名册 (数量即专家数, 默认 2)" },
226
+ supervisor: { type: "boolean", description: "监督者编排循环 (2026-10-03 接线): 同一任务派发给多个专家 → 分歧检测 → 监督者评审 (接受/打回带反馈重派) → 定稿整合。适合高要求决策/多角度论证需收敛结论的任务。默认 false。fix_rounds 控制最大轮数" },
227
+ },
228
+ },
229
+ execute: async (args, ctx) => {
230
+ const agent = ctx?.agent;
231
+ if (!agent) return "[工具错误] spawn_agent: 无 agent 上下文";
232
+ // 先校验参数, 再校验环境 (输入校验优先)
233
+ let tasks = null;
234
+ if (Array.isArray(args.tasks) && args.tasks.length) {
235
+ tasks = args.tasks.map((t) => String(t).slice(0, 4000));
236
+ } else if (args.task) {
237
+ tasks = [String(args.task).slice(0, 4000)];
238
+ }
239
+ if (!tasks) return "[工具错误] spawn_agent: 需要 task 或 tasks";
240
+ if (!agent.llm) return "[工具错误] spawn_agent: 主 agent 未配置模型, 无法委派";
241
+ // 懒建军团 (复用已有, 避免重复 spawn 进程)
242
+ let L = agent._legion;
243
+ if (!L) { L = new Legion(); agent._legion = L; }
244
+ // 角色名清洗: 保留中文 (中文向导项目, 侦察兵/分析师等中文角色名是常态), 只洗特殊字符
245
+ const role = String(args.role || "helper").replace(/[^\w\u4e00-\u9fff-]/g, "_").slice(0, 24);
246
+
247
+ const perspectives = Array.isArray(args.perspectives) ? args.perspectives.map((p) => String(p)).slice(0, tasks.length) : [];
248
+ // share_board 默认开: 子任务结论自动上板, 仲裁前自动读板 (显式 false 关闭)
249
+ const shareBoard = args.share_board !== false && !!board;
250
+
251
+ // 专家名册解析 (2026-10-02): experts 每任务 > expert 全局 > 无专家 (退回 role/perspectives)
252
+ // 未命中的专家键静默降级为无专家, 不炸委派。
253
+ const expertKeys = Array.isArray(args.experts) && args.experts.length
254
+ ? args.experts
255
+ : args.expert ? [args.expert] : [];
256
+ const expertList = tasks.map((_, i) => resolveExpert(expertKeys[i] ?? expertKeys[0]));
257
+ const effRoles = tasks.map((_, i) => expertList[i]?.name || role);
258
+ const effPersps = tasks.map((_, i) => perspectives[i] || expertList[i]?.perspective || null);
259
+
260
+ const boardTopic = role;
261
+
262
+ try {
263
+ // 监督者编排循环 (2026-10-03 接线, runSupervisor 首个产品入口):
264
+ // 同一任务 → 多专家并行 → 分歧检测 → 监督者评审 (接受/打回带反馈) → 定稿。默认 2 专家。
265
+ if (args.supervisor) {
266
+ const nExperts = Math.min(4, Math.max(2,
267
+ (Array.isArray(args.experts) && args.experts.length) || perspectives.length || 2));
268
+ const ts = Date.now().toString(36);
269
+ const names = [];
270
+ for (let i = 0; i < nExperts; i++) {
271
+ const nm = `${effRoles[i] || role}_sup_${ts}_${i}`;
272
+ const opts = { dataDir: path.join(agent.dataDir, "legion", nm), globalDataDir: agent.globalDataDir };
273
+ if (expertList[i]?.readonly) opts.env = { PPX_AGENT_READONLY: "1" }; // 只读专家防线对齐 review 循环
274
+ L.spawnAgent(nm, opts);
275
+ names.push(nm);
276
+ }
277
+ if (agent.lifecycle) agent.lifecycle.reproduce(nExperts);
278
+ const out = await runSupervisor({
279
+ legion: L,
280
+ agents: names,
281
+ task: tasks[0],
282
+ judge: args.judge || "",
283
+ llm: agent.auxLLM || agent.llm,
284
+ maxRounds: (() => { const n = Number(args.fix_rounds); return Number.isFinite(n) ? Math.min(Math.max(n, 1), 5) : 3; })(),
285
+ timeoutMs: DELEGATE_TIMEOUT_MS,
286
+ });
287
+ const head = out.divergent
288
+ ? `⚠️ 监督者编排 (${out.rounds} 轮, 一致率 ${(out.consensus * 100).toFixed(0)}%): 达轮数上限仍有分歧, 各方结论如下`
289
+ : `✅ 监督者编排 (${out.rounds} 轮, 一致率 ${(out.consensus * 100).toFixed(0)}%):`;
290
+ return `${head}\n\n${out.answer}`;
291
+ }
292
+
293
+ // SDD review 循环: 实施 -> 审查 -> 修复 -> 熔断
294
+ // 单任务: 直接跑; 多任务: 每个任务独立一对 (实施者+只读审查者), 并行跑, 可仲裁聚合
295
+ if (args.review) {
296
+ const prefix = `${role}_${Date.now().toString(36)}`;
297
+ if (tasks.length === 1) {
298
+ const out = await runReviewLoop({
299
+ agent, L, task: tasks[0],
300
+ perspective: effPersps[0],
301
+ role: effRoles[0], judge: args.judge,
302
+ fixRounds: args.fix_rounds,
303
+ namePrefix: `${prefix}_0`,
304
+ });
305
+ publishToBoard(board, { from: `${effRoles[0]}_impl`, topic: boardTopic, task: tasks[0], reply: out, status: out.startsWith("✅") ? "完成" : "熔断停放" });
306
+ return out;
307
+ }
308
+ // 多任务: 并行各任务 review, 各自独立 (agent 名唯一, 不冲突)
309
+ const settled = await Promise.all(tasks.map(async (task, i) => {
310
+ try {
311
+ const out = await runReviewLoop({
312
+ agent, L, task,
313
+ perspective: effPersps[i],
314
+ role: effRoles[i], judge: args.judge,
315
+ fixRounds: args.fix_rounds,
316
+ namePrefix: `${prefix}_${i}`,
317
+ });
318
+ publishToBoard(board, { from: `${effRoles[i]}_${i}_impl`, topic: boardTopic, task, reply: out, status: out.startsWith("✅") ? "完成" : "熔断停放" });
319
+ return out;
320
+ } catch (e) {
321
+ return `[子任务${i + 1} review 失败] ${e.message}`;
322
+ }
323
+ }));
324
+ if (args.arbitrate) {
325
+ return await arbitrateWithBoard(agent, tasks, settled, perspectives, args.judge, { board, shareBoard, boardTopic });
326
+ }
327
+ return tasks.map((t, i) => {
328
+ const p = perspectives?.[i] || expertList[i] ? ` (${expertList[i]?.name || ""}${perspectives[i] ? "·" + perspectives[i] : ""})` : "";
329
+ return `【子任务${i + 1}${p}】${t}\n${settled[i]}`;
330
+ }).join("\n\n");
331
+ }
332
+ // 并行 spawn 子 agent: 每个独立数据目录 + 独立视角; 只读专家 spawn 时禁修改工具
333
+ const names = tasks.map((_, i) => `${effRoles[i]}_${i}_${Date.now().toString(36)}`);
334
+ for (let i = 0; i < names.length; i++) {
335
+ L.spawnAgent(names[i], {
336
+ dataDir: path.join(agent.dataDir, "legion", names[i]),
337
+ globalDataDir: agent.globalDataDir,
338
+ env: expertList[i]?.readonly ? { PPX_AGENT_READONLY: "1" } : {},
339
+ });
340
+ }
341
+ // 生命周期: 繁衍计数 (ANS: reproducing)
342
+ if (agent.lifecycle) agent.lifecycle.reproduce(names.length);
343
+
344
+ // 并行派发, 全部等结果 (各自独立超时)
345
+ const settled = await Promise.all(tasks.map(async (task, i) => {
346
+ try {
347
+ const reply = await withTimeout(
348
+ L.send(names[i], { type: "chat", message: task, perspective: effPersps[i] }, { timeout: DELEGATE_TIMEOUT_MS + 5000 }),
349
+ DELEGATE_TIMEOUT_MS,
350
+ `子任务${i + 1}`
351
+ );
352
+ return { ok: true, reply: reply.reply || "(子 agent 无回复)" };
353
+ } catch (e) {
354
+ return { ok: false, reply: `[子任务${i + 1}失败] ${e.message}` };
355
+ }
356
+ }));
357
+ const results = settled.map((s) => s.reply);
358
+
359
+ // share_board: 每个子任务结论自动上板 (成功/失败都记, 状态区分)
360
+ if (shareBoard) {
361
+ settled.forEach((s, i) => {
362
+ publishToBoard(board, { from: names[i], topic: boardTopic, task: tasks[i], reply: s.reply, status: s.ok ? "完成" : "失败" });
363
+ });
364
+ }
365
+
366
+ // 单任务: 保持旧行为, 直接返回子 agent 回复
367
+ if (tasks.length === 1) return results[0];
368
+
369
+ // 多任务: 有 arbitrate 走主 agent 仲裁聚合, 否则拼接各方结果
370
+ if (args.arbitrate) {
371
+ const out = await arbitrateWithBoard(agent, tasks, results, perspectives, args.judge, { board, shareBoard, boardTopic });
372
+ return out;
373
+ }
374
+ return tasks.map((t, i) => {
375
+ const p = perspectives?.[i] || expertList[i] ? ` (${expertList[i]?.name || ""}${perspectives[i] ? "·" + perspectives[i] : ""})` : "";
376
+ return `【子任务${i + 1}${p}】${t}\n${results[i]}`;
377
+ }).join("\n\n");
378
+ } catch (e) {
379
+ return `[工具错误] spawn_agent: ${e.message}`;
380
+ }
381
+ },
382
+ });
383
+ }