@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
@@ -15,13 +15,15 @@
15
15
  * 注入每 turn 会膨胀 prompt)
16
16
  */
17
17
 
18
- import * as fs from "node:fs";
18
+
19
19
 
20
20
  import type {
21
21
  BeforeAgentStartEvent,
22
22
  BeforeAgentStartEventResult,
23
23
  ExtensionAPI,
24
24
  ExtensionContext,
25
+ SessionShutdownEvent,
26
+ SessionStartEvent,
25
27
  } from "@earendil-works/pi-coding-agent";
26
28
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
27
29
  import { getLogger } from "@zhushanwen/pi-extension-logger";
@@ -29,20 +31,30 @@ import { getLogger } from "@zhushanwen/pi-extension-logger";
29
31
  import {
30
32
  discoverResources,
31
33
  findWorkspaceRoot,
34
+ getCachedFileContent,
32
35
  } from "../shared/resource-discovery.ts";
36
+ import { parseResourceMeta } from "../shared/meta-parser.ts";
33
37
 
34
38
  const logger = getLogger("injector");
35
39
 
40
+ /**
41
+ * Session 级 workflow 列表缓存(per-process = per-session),与 agentCache 对称。
42
+ * 见 subagent-list-injector.ts 的 agentCache 注释。
43
+ */
44
+ let workflowCache: WorkflowEntry[] | null = null;
45
+
36
46
  /** 注入段中单个 workflow 的最大描述长度(控制每 turn prompt 体积) */
37
47
  const MAX_DESC_LEN = 160;
38
48
 
39
49
  /** 断句阈值比例:句末标点位置须 >= maxLen 的 40% 才采用,否则硬截断保留更多信息 */
40
50
  const DESC_BOUNDARY_MIN_RATIO = 0.4;
41
51
 
42
- /** 解析后的 workflow 条目(name + 截断后的 description */
52
+ /** 解析后的 workflow 条目(name + 截断后的 description + agentRef 路径) */
43
53
  export interface WorkflowEntry {
44
54
  name: string;
45
55
  description: string;
56
+ /** workflowRef:脚本 .js 文件的绝对路径(注入段 <location>,模型直接引用) */
57
+ path: string;
46
58
  }
47
59
 
48
60
  /**
@@ -68,46 +80,17 @@ export function summarizeDescription(
68
80
  }
69
81
 
70
82
  /**
71
- * 从源码中提取 `const meta = { ... }` 块(花括号配平)。
72
- * 无 meta 块返回 null。
73
- */
74
- function extractMetaBlock(src: string): string | null {
75
- const startMatch = src.match(/const\s+meta\s*=\s*\{/);
76
- if (!startMatch || startMatch.index === undefined) return null;
77
- const afterOpen = startMatch.index + startMatch[0].length;
78
- let depth = 1;
79
- let i = afterOpen;
80
- while (i < src.length && depth > 0) {
81
- const ch = src[i];
82
- if (ch === "{") depth++;
83
- else if (ch === "}") depth--;
84
- i++;
85
- }
86
- if (depth !== 0) return null;
87
- return src.slice(startMatch.index, i);
88
- }
89
-
90
- /** 从 meta 块中提取单行字符串字段值(双引号或单引号包裹)。 */
91
- function extractMetaField(block: string, field: string): string | null {
92
- const re = new RegExp(`^[ \\t]*${field}:\\s*("([^"]*)"|'([^']*)')`, "m");
93
- const m = block.match(re);
94
- if (!m) return null;
95
- return m[2] ?? m[3] ?? null;
96
- }
97
-
98
- /**
99
- * 解析 workflow .js/.mjs 文件的 meta 对象(name + description)。
83
+ * 解析 workflow .js 文件的 meta(name + description),经 IF1 parseResourceMeta。
100
84
  *
101
- * 所有 builtin workflow 均声明 `const meta = { name, description, phases }`,
102
- * description 为单行字符串。缺 name description 返回 null。
85
+ * m2 收敛:删 extractMetaBlock/extractMetaField(本地 brace-match parser),
86
+ * 改调 shared/meta-parser.ts parseResourceMeta(统一 parser)。仅认 @pi-meta 新格式。
87
+ * 投影到 WorkflowEntry {name, description(summarized)} 注入用。
103
88
  */
104
89
  export function parseWorkflowMeta(content: string): WorkflowEntry | null {
105
- const block = extractMetaBlock(content);
106
- if (!block) return null;
107
- const name = extractMetaField(block, "name");
108
- const description = extractMetaField(block, "description");
109
- if (!name || !description) return null;
110
- return { name, description: summarizeDescription(description) };
90
+ const meta = parseResourceMeta(content, "workflow");
91
+ if (!meta || meta.kind !== "workflow") return null;
92
+ // path discoverAllWorkflows 从 DiscoveredResource.path 填充
93
+ return { name: meta.name, description: summarizeDescription(meta.description), path: "" };
111
94
  }
112
95
 
113
96
  /**
@@ -129,9 +112,9 @@ export async function discoverAllWorkflows(
129
112
  for (const resource of resources) {
130
113
  if (!resource.available) continue;
131
114
  try {
132
- const content = fs.readFileSync(resource.path, "utf8");
115
+ const content = getCachedFileContent(resource.path) ?? "";
133
116
  const wf = parseWorkflowMeta(content);
134
- if (wf) map.set(wf.name, wf);
117
+ if (wf) map.set(wf.name, { ...wf, path: resource.path });
135
118
  } catch (err) {
136
119
  logger.error(
137
120
  `[workflow-list-injector] skip unreadable workflow file ${resource.path}`,
@@ -164,11 +147,13 @@ export function formatWorkflowList(workflows: WorkflowEntry[]): string {
164
147
 
165
148
  const lines = [
166
149
  "\n\n<available_workflows>",
167
- 'The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. Built-in workflows (chain/parallel/scatter-gather/map-reduce/review-fix-loop) run directly.',
150
+ // 引导语与具体 workflow 解耦:不写死内置名(列表本身已含全部 workflow,
151
+ // 名字/描述每 turn 由 @pi-meta 动态注入),只给通用路由指引 + read location 参数指针。
152
+ 'The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via action:run — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage).',
168
153
  ];
169
154
  for (const wf of workflows) {
170
155
  lines.push(
171
- ` <workflow><name>${escapeXml(wf.name)}</name><description>${escapeXml(wf.description)}</description></workflow>`,
156
+ ` <workflow><name>${escapeXml(wf.name)}</name><description>${escapeXml(wf.description)}</description><location>${escapeXml(wf.path)}</location></workflow>`,
172
157
  );
173
158
  }
174
159
  lines.push("</available_workflows>");
@@ -176,10 +161,30 @@ export function formatWorkflowList(workflows: WorkflowEntry[]): string {
176
161
  }
177
162
 
178
163
  /**
179
- * 注册 before_agent_start handler,注入 `<available_workflows>` 段。
180
- * 与 setupSubagentListInjector 链式(pi 串联多 handler 的 systemPrompt 返回值)。
164
+ * 注册 session 生命周期 handler,注入 `<available_workflows>` 段。
165
+ *
166
+ * 与 setupSubagentListInjector 对称:session_start 发现+缓存,before_agent_start
167
+ * 读缓存(miss fallback)渲染注入,session_shutdown 清缓存。与 subagent 注入
168
+ * handler 链式(pi 串联多 handler 的 systemPrompt 返回值)。
181
169
  */
182
170
  export function setupWorkflowListInjector(pi: ExtensionAPI): void {
171
+ pi.on(
172
+ "session_start",
173
+ async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
174
+ try {
175
+ workflowCache = await discoverAllWorkflows(
176
+ findWorkspaceRoot(ctx.cwd),
177
+ getAgentDir(),
178
+ );
179
+ } catch (err) {
180
+ // fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
181
+ logger.error("[workflow-list-injector] session_start discover failed", {
182
+ reason: err instanceof Error ? err.message : String(err),
183
+ });
184
+ }
185
+ },
186
+ );
187
+
183
188
  pi.on(
184
189
  "before_agent_start",
185
190
  async (
@@ -187,11 +192,14 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
187
192
  ctx: ExtensionContext,
188
193
  ): Promise<BeforeAgentStartEventResult | void> => {
189
194
  try {
190
- const workflows = await discoverAllWorkflows(
191
- findWorkspaceRoot(ctx.cwd),
192
- getAgentDir(),
193
- );
194
- const injection = formatWorkflowList(workflows);
195
+ // 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
196
+ if (workflowCache === null) {
197
+ workflowCache = await discoverAllWorkflows(
198
+ findWorkspaceRoot(ctx.cwd),
199
+ getAgentDir(),
200
+ );
201
+ }
202
+ const injection = formatWorkflowList(workflowCache);
195
203
  if (!injection) return;
196
204
  return { systemPrompt: event.systemPrompt + injection };
197
205
  } catch (err) {
@@ -201,4 +209,11 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
201
209
  }
202
210
  },
203
211
  );
212
+
213
+ pi.on(
214
+ "session_shutdown",
215
+ (_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
216
+ workflowCache = null;
217
+ },
218
+ );
204
219
  }
@@ -14,80 +14,137 @@
14
14
 
15
15
  import { describe, expect, it } from "vitest";
16
16
 
17
- import { findFlattenedArgKeys } from "../tool-workflow";
17
+ import { readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ import { argKeysFromMeta, findFlattenedArgKeys } from "../tool-workflow";
21
+ import { parseResourceMeta } from "../../shared/meta-parser.ts";
22
+
23
+ // 真实参数集(argKeysFromMeta 产物——m6 动态数据源,与 TC2 同源防漂移)
24
+ function rflKeys() {
25
+ const src = readFileSync(join(__dirname, "../../../workflows/review-fix-loop.js"), "utf-8");
26
+ const meta = parseResourceMeta(src, "workflow");
27
+ if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("rfl meta");
28
+ return argKeysFromMeta(meta.parameters);
29
+ }
30
+ function chainKeys() {
31
+ const src = readFileSync(join(__dirname, "../../../workflows/chain.js"), "utf-8");
32
+ const meta = parseResourceMeta(src, "workflow");
33
+ if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("chain meta");
34
+ return argKeysFromMeta(meta.parameters);
35
+ }
36
+ const RFL = rflKeys();
37
+ const CHAIN = chainKeys();
18
38
 
19
39
  describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
20
- it("triggers when args sub-fields flattened to top level", () => {
21
- expect(findFlattenedArgKeys({ action: "run", name: "chain", task: "x" })).toEqual(["task"]);
22
- expect(findFlattenedArgKeys({ action: "run", name: "x", items: ["a"] })).toEqual(["items"]);
40
+ it("TC3a: trigger——chain 参数集平铺 task 被识别;跨 workflow 区分(items 非 chain 参数不触发)", () => {
23
41
  expect(
24
- findFlattenedArgKeys({ action: "run", name: "x", task: "t", perspectives: ["p"] }),
25
- ).toEqual(["task", "perspectives"]);
42
+ findFlattenedArgKeys({ action: "run", name: "chain", task: "x" }, CHAIN.exact, CHAIN.patterns),
43
+ ).toEqual(["task"]);
44
+ // 跨 workflow:items 是 map-reduce 参数非 chain 参数 → 不触发(m6 动态集语义)
45
+ expect(
46
+ findFlattenedArgKeys({ action: "run", name: "chain", items: ["a"] }, CHAIN.exact, CHAIN.patterns),
47
+ ).toEqual([]);
26
48
  });
27
49
 
28
- it("does NOT trigger when fields correctly nested in args", () => {
50
+ it("TC3b: 嵌套 args 不触发", () => {
29
51
  expect(
30
- findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x", items: ["a"] } }),
52
+ findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x", items: ["a"] } }, CHAIN.exact, CHAIN.patterns),
31
53
  ).toEqual([]);
32
54
  });
33
55
 
34
- it("edge: key present at BOTH top-level and inside args is NOT flagged", () => {
35
- // 同时传 args.task 和顶层 task:args 已提供,顶层冗余被忽略,不算平铺。
36
- // 这是 reviewer 点名的 untested edge。
56
+ it("TC3c: 顶层 + args 内共存不触发(args-排除保留)", () => {
37
57
  expect(
38
- findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x" }, task: "y" }),
58
+ findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x" }, task: "y" }, CHAIN.exact, CHAIN.patterns),
39
59
  ).toEqual([]);
40
60
  });
41
61
 
42
- it("does NOT trigger when no known arg keys present", () => {
43
- expect(findFlattenedArgKeys({ action: "run", name: "x", args: {} })).toEqual([]);
44
- expect(findFlattenedArgKeys({ action: "status" })).toEqual([]);
62
+ it("TC3d: 无已知键不触发;空集不触发", () => {
63
+ expect(
64
+ findFlattenedArgKeys({ action: "run", name: "x", args: {} }, CHAIN.exact, CHAIN.patterns),
65
+ ).toEqual([]);
66
+ expect(
67
+ findFlattenedArgKeys({ action: "status" }, new Set(), []),
68
+ ).toEqual([]);
45
69
  });
46
70
 
47
- it("review-fix-loop fixAgent 平铺被识别(TC7)", () => {
48
- expect(findFlattenedArgKeys({ action: "run", name: "review-fix-loop", fixAgent: "worker" })).toEqual(["fixAgent"]);
71
+ it("TC3e: review-fix-loop fixAgent 平铺被识别(动态集 exact)", () => {
49
72
  expect(
50
- findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { fixAgent: "worker" } }),
73
+ findFlattenedArgKeys({ action: "run", name: "review-fix-loop", fixAgent: "worker" }, RFL.exact, RFL.patterns),
74
+ ).toEqual(["fixAgent"]);
75
+ expect(
76
+ findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { fixAgent: "worker" } }, RFL.exact, RFL.patterns),
51
77
  ).toEqual([]);
52
78
  });
53
79
 
54
- it("triggers for review-fix-loop args flattened to top level (incl. batchN prefix)", () => {
80
+ it("TC3f: batchN pattern 触发 + batchl 拼错不触发(数字后缀语义)", () => {
55
81
  expect(
56
- findFlattenedArgKeys({ action: "run", name: "review-fix-loop", targetType: "git-diff", target: "main" }),
82
+ findFlattenedArgKeys(
83
+ { action: "run", name: "review-fix-loop", targetType: "git-diff", target: "main" },
84
+ RFL.exact,
85
+ RFL.patterns,
86
+ ),
57
87
  ).toEqual(["targetType", "target"]);
58
88
  expect(
59
- findFlattenedArgKeys({ action: "run", name: "review-fix-loop", batch1: "reviewer", autoCommit: true }),
89
+ findFlattenedArgKeys(
90
+ { action: "run", name: "review-fix-loop", batch1: "reviewer", autoCommit: true },
91
+ RFL.exact,
92
+ RFL.patterns,
93
+ ),
60
94
  ).toEqual(["batch1", "autoCommit"]);
61
95
  expect(
62
- findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { targetType: "git-diff", target: "main" } }),
96
+ findFlattenedArgKeys(
97
+ { action: "run", name: "review-fix-loop", args: { targetType: "git-diff", target: "main" } },
98
+ RFL.exact,
99
+ RFL.patterns,
100
+ ),
101
+ ).toEqual([]);
102
+ // batchl 拼错(非数字后缀)不触发(m6 评审 C-1:pattern 自带数字后缀校验)
103
+ expect(
104
+ findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" }, RFL.exact, RFL.patterns),
63
105
  ).toEqual([]);
64
- // 未知键(如 batchl 拼错)不属于白名单,不触发平铺检测(由 workflow 内白名单校验报错)
65
- expect(findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" })).toEqual([]);
66
106
  });
67
107
 
68
- it("review-fix-loop 收敛/模型参数平铺被识别(S-13 补全 4 键)", () => {
108
+ it("TC3g: 收敛参数平铺被识别(动态集 exact——S-13 3 键,model 升格 TOOL_TOP_LEVEL 后排除)", () => {
69
109
  expect(
70
- findFlattenedArgKeys({
71
- action: "run",
72
- name: "review-fix-loop",
73
- model: "ds-flash",
74
- maxFixAttempts: 3,
75
- convergeNewIssues: 2,
76
- convergeRounds: 3,
77
- }),
78
- ).toEqual(["model", "maxFixAttempts", "convergeNewIssues", "convergeRounds"]);
110
+ findFlattenedArgKeys(
111
+ { action: "run", name: "review-fix-loop", model: "ds-flash", maxFixAttempts: 3, convergeNewIssues: 2, convergeRounds: 3 },
112
+ RFL.exact,
113
+ RFL.patterns,
114
+ ),
115
+ ).toEqual(["maxFixAttempts", "convergeNewIssues", "convergeRounds"]);
79
116
  expect(
80
- findFlattenedArgKeys({
81
- action: "run",
82
- name: "review-fix-loop",
83
- args: { model: "ds-flash", maxFixAttempts: 3 },
84
- convergeRounds: 3,
85
- }),
117
+ findFlattenedArgKeys(
118
+ { action: "run", name: "review-fix-loop", args: { model: "ds-flash", maxFixAttempts: 3 }, convergeRounds: 3 },
119
+ RFL.exact,
120
+ RFL.patterns,
121
+ ),
86
122
  ).toEqual(["convergeRounds"]);
87
123
  });
88
124
 
89
- it("returns [] for non-object input", () => {
90
- expect(findFlattenedArgKeys(null)).toEqual([]);
91
- expect(findFlattenedArgKeys(undefined)).toEqual([]);
125
+ it("TC3h: non-object 输入返回 []", () => {
126
+ expect(findFlattenedArgKeys(null, new Set(), [])).toEqual([]);
127
+ expect(findFlattenedArgKeys(undefined, new Set(), [])).toEqual([]);
128
+ });
129
+
130
+ it("TC3i: tool 顶层键不误报(M-3 回归——合成 parameters 直测 argKeysFromMeta 排除)", () => {
131
+ // 合成 parameters:workflow 声明参数 name(tool 键撞名)——argKeysFromMeta 必须排除
132
+ const keys = argKeysFromMeta({
133
+ type: "object",
134
+ properties: { name: { type: "string" }, task: { type: "string" } },
135
+ required: ["name"],
136
+ });
137
+ expect(keys.exact.has("task")).toBe(true);
138
+ expect(keys.exact.has("name")).toBe(false); // TOOL_TOP_LEVEL 排除(M-3 真回归锁定)
139
+ // 合法调用不误报
140
+ expect(
141
+ findFlattenedArgKeys(
142
+ { action: "run", name: "mywf", args: { name: "x" } },
143
+ keys.exact,
144
+ keys.patterns,
145
+ ),
146
+ ).toEqual([]);
147
+ // 真实 meta 不含 name(探针实测 16 键)——附加验证
148
+ expect(RFL.exact.has("name")).toBe(false);
92
149
  });
93
150
  });
@@ -101,18 +101,14 @@ describe("subagent tool description — 行为约束器(非功能说明书)"
101
101
  expect(DESCRIPTION).not.toContain('"sa_');
102
102
  });
103
103
 
104
- it("agent 枚举(schema 字段 description)包含全部 9 个内置 agent(含 orchestrator,防漏)", () => {
105
- // 包内有 9 agent .md(含 orchestrator)。schema agent 字段 description 必须全部列出,
106
- // 否则 LLM 无法选中未列出的 agent(功能回归)。cr-fix 防回归锁定。
107
- // 注意:agent 列表在 schema field description 里,不在主 description: 模板字符串里——
108
- // 断言源码全文(含 schema field description)而非 DESCRIPTION。
109
- const expected = [
110
- "general-purpose", "worker", "researcher", "explorer",
111
- "planner", "reviewer", "oracle", "context-builder", "orchestrator",
112
- ];
113
- for (const name of expected) {
114
- expect(SUBAGENT_TOOL_SRC).toContain(name);
115
- }
104
+ it("agent 字段 description 不写死枚举,指向 <available_subagents>(通用化防漂移)", () => {
105
+ // 原实现把 9 个内置 agent 名写死在 schema field description(防漏),
106
+ // 但枚举与包内 agents/*.md 存在漂移风险(新增/删除 agent 要手改两处)。
107
+ // 现改为指向每 turn 动态注入的 <available_subagents> 列表——防漏职责由注入段承担,
108
+ // 工具描述只保留通用指引(2026-08 通用化重构)。
109
+ expect(SUBAGENT_TOOL_SRC).toContain("available_subagents");
110
+ // 通用化约束:不写死任何具体 agent 名(名字随 agents/*.md 动态变化)
111
+ expect(SUBAGENT_TOOL_SRC).not.toMatch(/orchestrator|code-reviewer|context-builder/);
116
112
  });
117
113
 
118
114
  it("Anti-patterns 段明确 list/cancel 仍 nested(防过度泛化 flatten)", () => {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * actionGenerate 行为测试(m0 wave / TC1-TC7 + [P-generate-roundtrip])
3
+ *
4
+ * mock node:fs(mkdirSync/writeFileSync)避免真实落盘 .pi/workflows/.tmp/。
5
+ * parseResourceMetaDetailed 真实调用(不 mock m1)——round-trip 探针必须用真实 IF2。
6
+ *
7
+ * 框架:vitest(禁 node:test)。
8
+ */
9
+ import { mkdirSync, writeFileSync } from "node:fs";
10
+
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
+
13
+ import { actionGenerate, type ScriptParams, type TextContent } from "../tool-workflow-script.ts";
14
+
15
+ vi.mock("node:fs", () => ({
16
+ mkdirSync: vi.fn(),
17
+ writeFileSync: vi.fn(),
18
+ }));
19
+
20
+ const mockedWriteFileSync = vi.mocked(writeFileSync);
21
+ const mockedMkdirSync = vi.mocked(mkdirSync);
22
+
23
+ function gen(script: string, name = "test-wf"): ScriptParams {
24
+ return { action: "generate", name, script } as ScriptParams;
25
+ }
26
+
27
+ /** TextContent.text 在 content[0].text。 */
28
+ function textOf(r: TextContent): string {
29
+ return r.content[0]?.text ?? "";
30
+ }
31
+
32
+ const PI_META_VALID = `/* @pi-meta
33
+ name: test-wf
34
+ description: 合法新格式
35
+ phases: [a, b]
36
+ parameters:
37
+ type: object
38
+ properties:
39
+ task: { type: string }
40
+ required: [task]
41
+ */
42
+ const agent = require("./agent");
43
+ agent("worker", { task: $ARGS.task });
44
+ `;
45
+
46
+ const PI_META_MALFORMED = `/* @pi-meta
47
+ name: test-wf
48
+ description: bad
49
+ broken: indent
50
+ phases: [a]
51
+ */
52
+ const agent = require("./agent");
53
+ agent("w");
54
+ `;
55
+
56
+ const PI_META_REGEX_SINGLE_BS = `/* @pi-meta
57
+ name: test-wf
58
+ description: regex
59
+ phases: [A]
60
+ parameters:
61
+ type: object
62
+ patternProperties:
63
+ "^batch\\d+$": { type: string }
64
+ */
65
+ const agent = require("./agent");
66
+ agent("w");
67
+ `;
68
+
69
+ const LEGACY_CONST_META = `const meta = {
70
+ name: test-wf,
71
+ description: legacy,
72
+ phases: ["a"]
73
+ };
74
+ const agent = require("./agent");
75
+ agent("w");
76
+ `;
77
+
78
+ const NO_META = `const agent = require("./agent");
79
+ agent("w");
80
+ `;
81
+
82
+ const ESM_IMPORT = `/* @pi-meta
83
+ name: x
84
+ description: d
85
+ phases: [a]
86
+ */
87
+ import { foo } from "bar";
88
+ const agent = require("./agent");
89
+ agent("w");
90
+ `;
91
+
92
+ const NO_AGENT = `/* @pi-meta
93
+ name: x
94
+ description: d
95
+ phases: [a]
96
+ */
97
+ const x = 1;
98
+ `;
99
+
100
+ describe("actionGenerate (m0: @pi-meta 认可 + round-trip)", () => {
101
+ beforeEach(() => {
102
+ mockedWriteFileSync.mockClear();
103
+ mockedMkdirSync.mockClear();
104
+ });
105
+ afterEach(() => {
106
+ vi.clearAllMocks();
107
+ });
108
+
109
+ it("TC1: 合法 @pi-meta → ready + writeFileSync 被调用", () => {
110
+ const r = actionGenerate(gen(PI_META_VALID), undefined);
111
+ expect(r.isError).toBeFalsy();
112
+ expect(textOf(r)).toMatch(/ready|generated|test-wf/i);
113
+ expect(mockedWriteFileSync).toHaveBeenCalledTimes(1);
114
+ });
115
+
116
+ it("TC2: malformed @pi-meta YAML → error + writeFileSync 未调用 [P-generate-roundtrip]", () => {
117
+ const r = actionGenerate(gen(PI_META_MALFORMED), undefined);
118
+ expect(r.isError).toBe(true);
119
+ expect(textOf(r)).toMatch(/cannot be parsed|无法解析|line/i);
120
+ expect(mockedWriteFileSync).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it("TC3: legacy const meta(过渡期)→ ready + writeFileSync 被调用", () => {
124
+ const r = actionGenerate(gen(LEGACY_CONST_META), undefined);
125
+ expect(r.isError).toBeFalsy();
126
+ expect(mockedWriteFileSync).toHaveBeenCalledTimes(1);
127
+ });
128
+
129
+ it("TC4: 无 meta → error 提及 @pi-meta 新格式", () => {
130
+ const r = actionGenerate(gen(NO_META), undefined);
131
+ expect(r.isError).toBe(true);
132
+ expect(textOf(r)).toMatch(/meta declaration/i);
133
+ expect(textOf(r)).toMatch(/pi-meta/i);
134
+ expect(mockedWriteFileSync).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it("TC5: @pi-meta 单反斜杠正则 → error(LLM 高频错)[P-generate-roundtrip]", () => {
138
+ const r = actionGenerate(gen(PI_META_REGEX_SINGLE_BS), undefined);
139
+ expect(r.isError).toBe(true);
140
+ expect(textOf(r)).toMatch(/escape|cannot be parsed|无法解析|line/i);
141
+ expect(mockedWriteFileSync).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("TC6: ESM import → reject(保留现有行为)", () => {
145
+ const r = actionGenerate(gen(ESM_IMPORT), undefined);
146
+ expect(r.isError).toBe(true);
147
+ expect(textOf(r)).toMatch(/ESM|import/i);
148
+ });
149
+
150
+ it("TC7: 无 agent() → reject(保留现有行为)", () => {
151
+ const r = actionGenerate(gen(NO_AGENT), undefined);
152
+ expect(r.isError).toBe(true);
153
+ expect(textOf(r)).toMatch(/agent\(\)/i);
154
+ });
155
+
156
+ it("TC8: signal aborted → reject(保留现有行为)", () => {
157
+ const controller = new AbortController();
158
+ controller.abort();
159
+ const r = actionGenerate(gen(PI_META_VALID), controller.signal);
160
+ expect(r.isError).toBe(true);
161
+ expect(textOf(r)).toMatch(/abort/i);
162
+ });
163
+ });
@@ -23,13 +23,46 @@ const TOOL_WORKFLOW_SCRIPT_SRC = readFileSync(
23
23
  "utf-8",
24
24
  );
25
25
 
26
+ /** 截取 promptGuidelines 数组文本——防 KNOWN_ARG_KEYS 等注释/代码中的裸词污染断言。
27
+ * 数到数组闭合(跳过字符串字面量内的括号,exec-review F9:'],' 序列静默截断)。 */
28
+ function promptGuidelinesText(src: string): string {
29
+ const start = src.indexOf("promptGuidelines: [");
30
+ let depth = 0;
31
+ let inStr = false;
32
+ for (let i = start; i < src.length; i++) {
33
+ const ch = src[i];
34
+ if (inStr) {
35
+ if (ch === "\\") { i++; continue; }
36
+ if (ch === '"') inStr = false;
37
+ continue;
38
+ }
39
+ if (ch === '"') inStr = true;
40
+ else if (ch === "[") depth++;
41
+ else if (ch === "]") {
42
+ depth--;
43
+ if (depth === 0) return src.slice(start, i + 1);
44
+ }
45
+ }
46
+ return src.slice(start);
47
+ }
48
+
26
49
  describe("U1: workflow tool prompt mentions built-in workflows", () => {
27
- it("tool-workflow.ts description promptGuidelines 4 个内置 workflow 名称", () => {
28
- // 4 个内置通用编排 workflow 必须在提示词里出现,LLM 才能发现并使用。
29
- expect(TOOL_WORKFLOW_SRC).toContain("chain");
30
- expect(TOOL_WORKFLOW_SRC).toContain("parallel");
31
- expect(TOOL_WORKFLOW_SRC).toContain("scatter-gather");
32
- expect(TOOL_WORKFLOW_SRC).toContain("map-reduce");
50
+ it("TC4a: promptGuidelines 不含具体内置 args 枚举(m4 瘦身——参数知识在 read location)", () => {
51
+ // m4:BUILT-IN 枚举删除,发现职责转移给 <available_workflows> 注入段 + read location。
52
+ // 截取 promptGuidelines 段断言("batch1..batchN" 等在 KNOWN_ARG_KEYS 注释中出现)。
53
+ const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
54
+ expect(guidelines).not.toContain("chain (sequential");
55
+ expect(guidelines).not.toContain("args: task");
56
+ expect(guidelines).not.toContain("batch1..batchN");
57
+ expect(guidelines).not.toContain("chain/parallel/scatter-gather/map-reduce");
58
+ });
59
+
60
+ it("TC4b: promptGuidelines 引导 read location 获取参数细节(info 已砍,ADR-0003 D5)", () => {
61
+ const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
62
+ expect(guidelines).toContain("read the <location>");
63
+ expect(guidelines).toContain("script file");
64
+ // info action 已砍,引导语不再含 workflow info
65
+ expect(guidelines).not.toContain("workflow info");
33
66
  });
34
67
 
35
68
  it("tool-workflow.ts promptGuidelines 含 workflow-script list 交叉引用", () => {
@@ -58,11 +91,14 @@ describe("U1: workflow tool prompt mentions built-in workflows", () => {
58
91
  expect(TOOL_WORKFLOW_SRC).toContain("top level");
59
92
  });
60
93
 
61
- it("runtime handler 错误文案含 Correct 纠正正例 + 平铺检测", () => {
94
+ it("runtime handler 错误文案含 Correct 纠正正例 + 平铺检测(m6:动态参数集)", () => {
62
95
  // 读源码文本断言 actionRun/必填校验的错误文案含 Correct 正例,
63
- // 让弱模型撞错后第二次能直接照抄正确形态。KNOWN_ARG_KEYS 证明平铺检测存在。
96
+ // 让弱模型撞错后第二次能直接照抄正确形态。m6 后平铺检测数据源为动态
97
+ // argKeysFromMeta(schema 即 SSOT)——断言新机制存在(KNOWN_ARG_KEYS 已删)。
64
98
  expect(TOOL_WORKFLOW_SRC).toContain("Correct:");
65
- expect(TOOL_WORKFLOW_SRC).toContain("KNOWN_ARG_KEYS");
99
+ expect(TOOL_WORKFLOW_SRC).toContain("findFlattenedArgKeys");
100
+ expect(TOOL_WORKFLOW_SRC).toContain("argKeysFromMeta");
101
+ expect(TOOL_WORKFLOW_SRC).not.toMatch(/const\s+KNOWN_ARG_KEYS/);
66
102
  });
67
103
 
68
104
  it("tool-workflow-script.ts list action 的 promptGuidelines 含 workflow run 交叉引用", () => {
@@ -80,4 +116,14 @@ describe("U1: workflow tool prompt mentions built-in workflows", () => {
80
116
  expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("CRITICAL ANTI-PATTERN");
81
117
  expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("NEVER generate");
82
118
  });
119
+
120
+ it("promptGuidelines + parameter description 标注 budget 默认不限制(B/C:除非用户要求否则别设)", () => {
121
+ // budget 是 run 级参数,运行时 maxTokens===undefined → 不限制(budget.ts isExceeded 守卫)。
122
+ // 但 call shape 示例展示了 tokens/time 字段,LLM 会误以为每次都该填。
123
+ // promptGuidelines 必须明确 "Do NOT set ... unless user explicitly requests",
124
+ // parameter description 同步标 "omit = unlimited",双重约束压过示例的反引导。
125
+ const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
126
+ expect(guidelines).toContain("Do NOT set tokens/time unless the user explicitly requests");
127
+ expect(TOOL_WORKFLOW_SRC).toContain("omit = unlimited (default)");
128
+ });
83
129
  });