@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
@@ -111,6 +111,10 @@ interface RunOptions {
111
111
  timeoutMs?: number;
112
112
  /** workerData.callCache 预填(测缓存命中路径)。 */
113
113
  callCache?: Map<number, unknown>;
114
+ /** Run 级 model override(透传到 workerData.model → $MODEL global)。 */
115
+ model?: string;
116
+ /** Run 级 thinkingLevel override(透传到 workerData.thinkingLevel → $THINKING_LEVEL global)。 */
117
+ thinkingLevel?: string;
114
118
  }
115
119
 
116
120
  /**
@@ -132,6 +136,9 @@ function runWorker(userScript: string, opts: RunOptions = {}): Promise<RunResult
132
136
  callCache: opts.callCache instanceof Map
133
137
  ? Object.fromEntries(opts.callCache)
134
138
  : opts.callCache ?? {},
139
+ // Option B run-level override:透传到 $MODEL/$THINKING_LEVEL worker global
140
+ model: opts.model,
141
+ thinkingLevel: opts.thinkingLevel,
135
142
  },
136
143
  });
137
144
  // S8:创建后立即登记,afterEach 兜底清理(防止 promise 泄漏导致 Worker 未终止)
@@ -449,3 +456,65 @@ describe("buildWorkerScript runtime — W2 agent() returnMeta mode", () => {
449
456
  });
450
457
  });
451
458
 
459
+ // ── P3/P4 runtime: run-level model/thinkingLevel override 真实注入(L0→L1 升级)──
460
+ // worker-script-builder.test.ts 的 P3/P4 block 全是源码字符串断言(L0:验证「生成的
461
+ // 源码含 $MODEL 注入行」)。本组起真实 Worker 线程,传 workerData.model/thinkingLevel,
462
+ // 断言 agent() 三分支产出的 agent-call 消息 opts.model/opts.thinkingLevel 真实继承
463
+ // $MODEL/$THINKING_LEVEL global(L1:验证「运行时 agent-call 携带正确 override」)。
464
+ // 对应 docs/testing/ 断言价值层级 L1(真实 Worker 产物,非源码字符串)。
465
+
466
+ describe("buildWorkerScript runtime — P3/P4 run-level model/thinkingLevel override 真实注入", () => {
467
+ it("workerData.model 经 $MODEL global 注入到 object 分支 agent() opts.model", async () => {
468
+ const script = `await agent({ prompt: "hi" }); return { done: true };`;
469
+ const res = await runWorker(script, { model: "anthropic/claude-sonnet-4-5" });
470
+ expect(res.agentCalls).toHaveLength(1);
471
+ expect(res.agentCalls[0]!.opts.model).toBe("anthropic/claude-sonnet-4-5");
472
+ expect(res.workerError).toBeUndefined();
473
+ expect(res.exitCode).not.toBe(1);
474
+ });
475
+
476
+ it("workerData.thinkingLevel 经 $THINKING_LEVEL global 注入到 opts.thinkingLevel", async () => {
477
+ const script = `await agent({ prompt: "hi" }); return { done: true };`;
478
+ const res = await runWorker(script, { thinkingLevel: "high" });
479
+ expect(res.agentCalls).toHaveLength(1);
480
+ expect(res.agentCalls[0]!.opts.thinkingLevel).toBe("high");
481
+ expect(res.workerError).toBeUndefined();
482
+ });
483
+
484
+ it("per-call model 优先于 $MODEL global(显式传 model 时不被 override 覆盖)", async () => {
485
+ const script = `await agent({ prompt: "hi", model: "openai/gpt-4o" }); return {};`;
486
+ const res = await runWorker(script, { model: "anthropic/claude-sonnet-4-5" });
487
+ expect(res.agentCalls).toHaveLength(1);
488
+ expect(res.agentCalls[0]!.opts.model).toBe("openai/gpt-4o");
489
+ });
490
+
491
+ it("agent() 三分支一致继承 $MODEL(string / task / object.prompt)", async () => {
492
+ const script = `
493
+ await agent("str-branch");
494
+ await agent({ task: "t" });
495
+ await agent({ prompt: "obj" });
496
+ return {};
497
+ `;
498
+ const res = await runWorker(script, {
499
+ model: "X/Y",
500
+ agentResults: [{}, {}, {}],
501
+ });
502
+ expect(res.agentCalls).toHaveLength(3);
503
+ expect(res.agentCalls.map((c) => c.opts.model)).toEqual(["X/Y", "X/Y", "X/Y"]);
504
+ });
505
+
506
+ it("model+thinkingLevel 同时注入(Option B 对称验证)", async () => {
507
+ const script = `await agent({ prompt: "hi" }); return {};`;
508
+ const res = await runWorker(script, { model: "p/m", thinkingLevel: "max" });
509
+ expect(res.agentCalls[0]!.opts.model).toBe("p/m");
510
+ expect(res.agentCalls[0]!.opts.thinkingLevel).toBe("max");
511
+ });
512
+
513
+ it("不传 model 时 opts.model 为 undefined(零配置默认继承主 agent,不误注入)", async () => {
514
+ const script = `await agent({ prompt: "hi" }); return {};`;
515
+ const res = await runWorker(script);
516
+ expect(res.agentCalls).toHaveLength(1);
517
+ expect(res.agentCalls[0]!.opts.model).toBeUndefined();
518
+ });
519
+ });
520
+
@@ -237,7 +237,7 @@ describe("buildWorkerScript — agent() thinkingLevel passthrough (3 branches)",
237
237
  const stringBranch = script.match(/typeof firstArg === "string"[\s\S]*?\};/);
238
238
  expect(stringBranch).toBeTruthy();
239
239
  expect(stringBranch![0]).toContain(
240
- "thinkingLevel: (secondArg && typeof secondArg === \"object\" && secondArg.thinkingLevel) || undefined",
240
+ "thinkingLevel: (secondArg && typeof secondArg === \"object\" && secondArg.thinkingLevel) || $THINKING_LEVEL",
241
241
  );
242
242
  });
243
243
 
@@ -257,3 +257,53 @@ describe("buildWorkerScript — agent() thinkingLevel passthrough (3 branches)",
257
257
  });
258
258
  });
259
259
 
260
+ // ── P3/P4: run-level model/thinkingLevel override global injection ──
261
+ // Option B 对称单路径:workflow 顶层 model/thinkingLevel 经 workerData →
262
+ // $MODEL/$THINKING_LEVEL worker global → agent() 三分支 fallback。
263
+ // 验证生成的 worker 源码字符串包含 global 注入与三分支 fallback 逻辑。
264
+ // 运行时真生效需 worker 集成 harness(设计文档 §6 降级为源码字符串断言)。
265
+
266
+ describe("P3/P4 run-level model/thinkingLevel global injection + agent() fallback", () => {
267
+ const script = buildWorkerScript("// noop user script");
268
+
269
+ it("injects $MODEL global from workerData.model", () => {
270
+ expect(script).toContain('const $MODEL = (workerData.model && typeof workerData.model === "string") ? workerData.model : undefined;');
271
+ });
272
+
273
+ it("injects $THINKING_LEVEL global from workerData.thinkingLevel", () => {
274
+ expect(script).toContain('const $THINKING_LEVEL = (workerData.thinkingLevel && typeof workerData.thinkingLevel === "string") ? workerData.thinkingLevel : undefined;');
275
+ });
276
+
277
+ it("string branch falls back to $MODEL when secondArg.model omitted", () => {
278
+ const stringBranch = script.match(/typeof firstArg === "string"[\s\S]*?\};/);
279
+ expect(stringBranch).toBeTruthy();
280
+ expect(stringBranch![0]).toContain('secondArg.model) || $MODEL');
281
+ });
282
+
283
+ it("string branch falls back to $THINKING_LEVEL when secondArg.thinkingLevel omitted", () => {
284
+ const stringBranch = script.match(/typeof firstArg === "string"[\s\S]*?\};/);
285
+ expect(stringBranch).toBeTruthy();
286
+ expect(stringBranch![0]).toContain('secondArg.thinkingLevel) || $THINKING_LEVEL');
287
+ });
288
+
289
+ it("task/agent branch falls back to $MODEL when firstArg.model omitted", () => {
290
+ const taskAgentBranch = script.match(/firstArg\.task \|\| firstArg\.agent[\s\S]*?\};/);
291
+ expect(taskAgentBranch).toBeTruthy();
292
+ expect(taskAgentBranch![0]).toContain('model: firstArg.model || $MODEL');
293
+ });
294
+
295
+ it("task/agent branch falls back to $THINKING_LEVEL when firstArg.thinkingLevel omitted", () => {
296
+ const taskAgentBranch = script.match(/firstArg\.task \|\| firstArg\.agent[\s\S]*?\};/);
297
+ expect(taskAgentBranch).toBeTruthy();
298
+ expect(taskAgentBranch![0]).toContain('thinkingLevel: firstArg.thinkingLevel || $THINKING_LEVEL');
299
+ });
300
+
301
+ it("object branch injects $MODEL when opts.model omitted (guard fallback)", () => {
302
+ expect(script).toContain('if (!opts.model && $MODEL) opts.model = $MODEL;');
303
+ });
304
+
305
+ it("object branch injects $THINKING_LEVEL when opts.thinkingLevel omitted (guard fallback)", () => {
306
+ expect(script).toContain('if (!opts.thinkingLevel && $THINKING_LEVEL) opts.thinkingLevel = $THINKING_LEVEL;');
307
+ });
308
+ });
309
+
@@ -122,12 +122,12 @@ function makeDoneChildRun(opts: {
122
122
  function makeDeps(opts: {
123
123
  script?: WorkflowScript;
124
124
  childRun?: WorkflowRun;
125
- registry?: { get: ReturnType<typeof vi.fn> };
125
+ registry?: { getPath: ReturnType<typeof vi.fn> };
126
126
  } = {}): LauncherDeps {
127
127
  const runs = new Map<string, WorkflowRun>();
128
128
  if (opts.childRun) runs.set(MOCK_RUN_ID, opts.childRun);
129
129
  const registry = opts.registry ?? {
130
- get: vi.fn(async () => opts.script),
130
+ getPath: vi.fn(async () => opts.script),
131
131
  };
132
132
  return {
133
133
  registry,
@@ -24,7 +24,7 @@
24
24
  * 内容 + 手动构造 WorkflowScript 对象,包装为一个满足 WorkflowScriptRegistry 接口
25
25
  * 的自定义 registry(loadWorkflowsFromDir)。
26
26
  */
27
- import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
27
+ import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
28
28
  import { tmpdir } from "node:os";
29
29
  import { dirname, join } from "node:path";
30
30
  import { fileURLToPath } from "node:url";
@@ -33,6 +33,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33
33
 
34
34
  import { JsonlRunStore } from "../jsonl-run-store.ts";
35
35
  import { type LauncherDeps,runAndWait } from "../launcher.ts";
36
+ import { actionRun } from "../../interface/tool-workflow.ts";
36
37
  import type { LifecycleDeps } from "../models/ports.ts";
37
38
  import type { AgentRunner } from "../models/ports.ts";
38
39
  import type { AgentResult, AgentUsage } from "../models/types.ts";
@@ -41,6 +42,8 @@ import {
41
42
  WorkflowScript,
42
43
  type WorkflowSource,
43
44
  } from "../models/workflow-script.ts";
45
+ import { parseResourceMeta } from "../../shared/meta-parser.ts";
46
+ import { normalizeRef } from "../../shared/agent-ref.ts";
44
47
  import type { WorkflowScriptRegistry } from "../models/workflow-script-registry.ts";
45
48
  import { WorkerHostImpl } from "../worker-host.ts";
46
49
 
@@ -49,6 +52,10 @@ import { WorkerHostImpl } from "../worker-host.ts";
49
52
  // 即 __dirname → .. (orchestration) → .. (src) → .. (subagent-workflow) → workflows
50
53
  const __dirname = dirname(fileURLToPath(import.meta.url));
51
54
  const WORKFLOWS_DIR = join(__dirname, "..", "..", "..", "workflows");
55
+ const AGENTS_DIR = join(__dirname, "..", "..", "..", "agents");
56
+ // S2:workflowRef/agentRef = 绝对路径(注入段 <location> 同源)
57
+ const wf = (name: string): string => join(WORKFLOWS_DIR, name + ".js");
58
+ const agentMd = (name: string): string => join(AGENTS_DIR, name + ".md");
52
59
 
53
60
  // ── 临时 session 目录(RunStore 持久化根),每用例重建 ──────────────────
54
61
  let sessionDir: string;
@@ -147,25 +154,12 @@ function makeMockRunner(): AgentRunner & { run: ReturnType<typeof vi.fn> } {
147
154
  * 同语义,避免执行用户代码)。失败时回落到 name=文件名 stem 的空 meta。
148
155
  */
149
156
  function extractMeta(source: string, fallbackName: string): WorkflowMeta {
150
- const metaPattern = /(?:export\s+)?const\s+meta\s*=\s*(\{[^]*?\});?\s*$/m;
151
- const match = metaPattern.exec(source);
152
- if (match) {
153
- try {
154
- const fn = new Function(`return (${match[1]});`);
155
- const obj = fn();
156
- if (obj && typeof obj === "object" && typeof obj.name === "string") {
157
- return {
158
- name: obj.name,
159
- description: typeof obj.description === "string" ? obj.description : "",
160
- phases: Array.isArray(obj.phases) ? obj.phases : [],
161
- };
162
- }
163
- } catch (e) {
164
- // meta 提取失败(非法 JS / regex 不匹配)→ 回落 fallback name,非测试关注点
165
- void e;
166
- }
167
- }
168
- return { name: fallbackName, description: "", phases: [] };
157
+ // m2 exec-review MINOR-1:旧 const meta regex + new Function 随 m2 迁移已失效(恒走空
158
+ // fallback,不再验证真实 meta 提取),改调 IF1 parseResourceMeta(与
159
+ // builtin-workflows-structure.test 一致)。失败时回落到 name=文件名 stem 的空 meta。
160
+ const meta = parseResourceMeta(source, "workflow");
161
+ if (meta && meta.kind === "workflow") return meta;
162
+ return { kind: "workflow", name: fallbackName, description: "", phases: [] };
169
163
  }
170
164
 
171
165
  /**
@@ -208,6 +202,29 @@ function loadWorkflowsFromDir(dir: string): Map<string, WorkflowScript> {
208
202
  function makeRegistry(scripts: Map<string, WorkflowScript>): WorkflowScriptRegistry {
209
203
  return {
210
204
  get: async (name: string) => scripts.get(name),
205
+ // S2:按路径加载(任意路径 .js,不限扫描目录)——路径未预扫则直接读文件
206
+ getPath: async (ref: string) => {
207
+ const normalized = normalizeRef(ref, ".js");
208
+ if (normalized === null) return undefined;
209
+ for (const script of scripts.values()) {
210
+ if (script.path === normalized) return script;
211
+ }
212
+ try {
213
+ const sourceCode = readFileSync(normalized, "utf-8");
214
+ const stem = basename(normalized, ".js");
215
+ const meta = extractMeta(sourceCode, stem);
216
+ return new WorkflowScript({
217
+ name: meta.name,
218
+ source: "saved",
219
+ path: normalized,
220
+ sourceCode,
221
+ meta,
222
+ available: true,
223
+ });
224
+ } catch {
225
+ return undefined;
226
+ }
227
+ },
211
228
  loadAll: async () => Array.from(scripts.values()),
212
229
  invalidate: () => {},
213
230
  };
@@ -292,7 +309,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
292
309
  async () => {
293
310
  const deps = makeDeps();
294
311
  const result = await runAndWait(
295
- "parallel",
312
+ wf("parallel"),
296
313
  { target: "src/auth/login.ts" },
297
314
  deps,
298
315
  undefined,
@@ -315,7 +332,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
315
332
  async () => {
316
333
  const deps = makeDeps();
317
334
  const result = await runAndWait(
318
- "chain",
335
+ wf("chain"),
319
336
  { task: "把这段需求文档拆成技术任务" },
320
337
  deps,
321
338
  undefined,
@@ -338,7 +355,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
338
355
  async () => {
339
356
  const deps = makeDeps();
340
357
  const result = await runAndWait(
341
- "map-reduce",
358
+ wf("map-reduce"),
342
359
  { operation: "审查代码风格", items: ["file1.ts", "file2.ts"] },
343
360
  deps,
344
361
  undefined,
@@ -363,7 +380,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
363
380
  async () => {
364
381
  const deps = makeDeps();
365
382
  const result = await runAndWait(
366
- "scatter-gather",
383
+ wf("scatter-gather"),
367
384
  { task: "重构认证模块,涉及 session/jwt/oauth 三块" },
368
385
  deps,
369
386
  undefined,
@@ -383,4 +400,140 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
383
400
  },
384
401
  RUN_TIMEOUT_MS,
385
402
  );
403
+
404
+ it(
405
+ "TC10: runAndWait 参数校验失败 → reason=invalid_args + runId='' + info 指引(chokepoint 先拦)",
406
+ async () => {
407
+ // review-fix-loop 有 parameters schema(唯一带 schema 的内置 workflow)——
408
+ // 缺 required targetType/target → chokepoint 在 worker 启动前拦截。
409
+ const deps = makeDeps();
410
+ const result = await runAndWait(
411
+ wf("review-fix-loop"),
412
+ { batch1: agentMd("code-reviewer") },
413
+ deps,
414
+ undefined,
415
+ RUN_TIMEOUT_MS,
416
+ );
417
+
418
+ expect(result.status).toBe("done");
419
+ expect(result.reason).toBe("invalid_args");
420
+ expect(result.runId).toBe("");
421
+ expect(result.error).toContain("Invalid args for workflow 'review-fix-loop'");
422
+ expect(result.error).toContain("targetType");
423
+ expect(result.error).toContain("Read the workflow script file");
424
+ },
425
+ RUN_TIMEOUT_MS,
426
+ );
427
+
428
+ it(
429
+ "TC12: coerce 结果到达 worker(chokepoint 原地 coerce → $ARGS 收到 boolean false)",
430
+ async () => {
431
+ // fixture 必须过 lint 入口检查(含 agent( 调用)+ typecheckMeta(name/description/phases)
432
+ const fixtureDir = mkdtempSync(join(tmpdir(), "wf-tc12-"));
433
+ try {
434
+ writeFileSync(
435
+ join(fixtureDir, "args-probe.js"),
436
+ [
437
+ "/* @pi-meta",
438
+ "name: args-probe",
439
+ "description: TC12 coerce probe fixture",
440
+ "phases: [a]",
441
+ "parameters:",
442
+ " type: object",
443
+ " properties:",
444
+ " autoCommit: { type: boolean }",
445
+ " required: [autoCommit]",
446
+ "*/",
447
+ 'module.exports.execute = async ({ agent, $ARGS }) => {',
448
+ ' await agent({ prompt: "x" });',
449
+ " return { t: typeof $ARGS.autoCommit, v: $ARGS.autoCommit };",
450
+ "};",
451
+ "",
452
+ ].join("\n"),
453
+ "utf-8",
454
+ );
455
+ const scripts = loadWorkflowsFromDir(fixtureDir);
456
+ const deps = { ...makeDeps(), registry: makeRegistry(scripts) };
457
+
458
+ const result = await runAndWait(
459
+ join(fixtureDir, "args-probe.js"),
460
+ { autoCommit: "false" },
461
+ deps,
462
+ undefined,
463
+ RUN_TIMEOUT_MS,
464
+ );
465
+
466
+ expect(result.reason).toBe("completed");
467
+ const outcome = result.scriptResult as { t: string; v: unknown };
468
+ expect(outcome.t).toBe("boolean");
469
+ expect(outcome.v).toBe(false);
470
+ } finally {
471
+ rmSync(fixtureDir, { recursive: true, force: true });
472
+ }
473
+ },
474
+ RUN_TIMEOUT_MS,
475
+ );
476
+
477
+ it("TC4: actionRun 顺序——not_found 优先(平铺 + 不存在名);slug 护栏保留;chain 平铺 Correct 文案", async () => {
478
+ const deps = makeDeps();
479
+ // 平铺 + 不存在 name → not_found 优先(m6:registry.get 先于平铺检测)
480
+ const notFound = await actionRun(
481
+ { action: "run", name: wf("nope"), task: "x" },
482
+ deps,
483
+ undefined,
484
+ );
485
+ expect(notFound.isError).toBe(true);
486
+ expect(notFound.content![0]!.text).toContain("not found"); // DoD 用户可见断言
487
+ // R5(D7):workflow not_found 文案含 <available_workflows> <location> 恢复指引
488
+ expect(notFound.content![0]!.text).toContain("<available_workflows>");
489
+ expect(notFound.content![0]!.text).toContain("<location>");
490
+ // chain 平铺 task → isError Correct 正例(动态集来自 registry)
491
+ const flat = await actionRun(
492
+ { action: "run", name: wf("chain"), task: "x" },
493
+ deps,
494
+ undefined,
495
+ );
496
+ expect(flat.isError).toBe(true);
497
+ expect(flat.content![0]!.text).toContain("Detected task at top level");
498
+ expect(flat.content![0]!.text).toContain("Correct:");
499
+ // slug 护栏保留(m6 顺序调整后仍在 runWorkflow 前)
500
+ const slug = await actionRun(
501
+ { action: "run", name: wf("chain"), args: { task: "x" }, slug: "a".repeat(40) },
502
+ deps,
503
+ undefined,
504
+ );
505
+ expect(slug.isError).toBe(true);
506
+ expect(slug.content![0]!.text).toContain("slug exceeds");
507
+ });
508
+
509
+ it("TC6: 跨 workflow 平铺语义——review-fix-loop 平铺 task(非其参数)走 args-validator", async () => {
510
+ const deps = makeDeps();
511
+ // task 是 chain 参数非 review-fix-loop 参数 → 不报平铺 → args 缺 targetType
512
+ // → m3 chokepoint invalid_args(错误更准——评审 m-5 语义锁定)
513
+ const result = await actionRun(
514
+ { action: "run", name: wf("review-fix-loop"), task: "x" },
515
+ deps,
516
+ undefined,
517
+ );
518
+ expect(result.isError).toBe(true);
519
+ expect(result.content![0]!.text).toContain("Invalid args for workflow 'review-fix-loop'");
520
+ expect(result.content![0]!.text).toContain("targetType");
521
+ });
522
+
523
+ it("TC9: actionRun 参数校验失败 → isError ToolResult + §5.3 指引", async () => {
524
+ const deps = makeDeps();
525
+ const result = await actionRun(
526
+ { action: "run", name: wf("review-fix-loop"), args: { batch1: agentMd("code-reviewer") } },
527
+ deps,
528
+ undefined,
529
+ );
530
+
531
+ expect(result.isError).toBe(true);
532
+ const text = result.content?.[0]?.text ?? "";
533
+ expect(text).toContain("Invalid args for workflow 'review-fix-loop'");
534
+ expect(text).toContain("targetType");
535
+ expect(text).toContain("Read the workflow script file");
536
+ });
537
+
538
+
386
539
  });
@@ -1,83 +1,48 @@
1
1
  /**
2
- * Agent options resolver — resolves agent name / skill / schema to system
3
- * prompt files + env vars on every dispatch (BL-1).
2
+ * Agent options resolver — resolves skill / schema to append-system-prompt
3
+ * content + env vars on every dispatch (BL-1).
4
4
  *
5
- * BL-1:解析 workflow 脚本里 `agent({agent,skill,schema})` 的 inline override,
6
- * 否则 pi 子进程只收到原始 prompt,没有 --append-system-prompt / --skill /
7
- * PI_WORKFLOW_SCHEMA。AgentCallOpts 从 engine/models/types 引入。
5
+ * BL-1:解析 workflow 脚本里 `agent({skill,schema})` 的 inline override,
6
+ * 否则 pi 子进程只收到原始 prompt,没有 --append-system-prompt /
7
+ * --skill / PI_WORKFLOW_SCHEMA。
8
+ *
9
+ * 职责范围(M2 修正):仅处理 schema SO 指令(内容直传 appendSystemPrompt)+ skill。
10
+ * agent ref 处理(systemPrompt/model/thinkingLevel)已移交 resolveIdentity(execution 层,
11
+ * 经 getAgentConfig + resolveModel 完整覆盖),消除双重注入与 model 层级混乱。
8
12
  *
9
13
  * 调用方:engine/error-recovery.ts dispatchAgentCall(每次 agent-call 消息)。
10
- * - agent → AgentRegistry.resolve → systemPrompt 写临时文件 → systemPromptFiles(--append-system-prompt)
11
14
  * - skill → resolveSkillPath → skillPath(--skill)
12
- * - schema → 结构化输出指令写临时文件 systemPromptFiles + schemaEnv(PI_WORKFLOW_SCHEMA)
15
+ * - schema → 结构化输出指令内容直传 appendSystemPrompt(--append-system-prompt)+ schemaEnv(PI_WORKFLOW_SCHEMA)
13
16
  *
14
- * 临时文件在 activeTempFiles 集合注册,session_shutdown 时由 cleanupAllTempFiles 统一回收。
17
+ * M2 bug 修正:旧实现把 schema 指令写成临时文件、push 文件路径(而非内容)给下游,
18
+ * 下游 mapper/session-runner 把路径当文本拼进最终 append 文件,导致 schema 指令从未
19
+ * 进入子进程。改为指令内容直传(不写盘、无临时文件)。
15
20
  */
16
21
 
17
- import { randomUUID } from "node:crypto";
18
- import * as fs from "node:fs";
19
- import * as path from "node:path";
20
-
21
22
  import type { AgentCallOpts } from "./models/types.ts";
22
- import type { AgentRegistry } from "../execution/agent-registry.ts"; // type-only(本文件不 new,只接收实例参数)
23
23
  import { resolveSkillPath } from "./skill-discovery.ts";
24
24
 
25
- const UUID_SLICE_LEN = 8;
26
-
27
25
  export interface ResolveResult {
28
26
  opts: AgentCallOpts;
29
27
  error?: string;
30
28
  }
31
29
 
32
30
  /**
33
- * Resolve agent name and schema into systemPromptFiles + skillPath + schemaEnv.
31
+ * Resolve skill and schema into appendSystemPrompt (content array) + skillPath + schemaEnv.
34
32
  *
35
- * - Agent systemPrompt -> temp file via --append-system-prompt
36
33
  * - Skill name -> resolved SKILL.md dir path via --skill
37
- * - Schema JSON -> temp file with structured-output instruction + PI_WORKFLOW_SCHEMA env
34
+ * - Schema JSON -> structured-output instruction string pushed into appendSystemPrompt
35
+ * (content, not file path) + PI_WORKFLOW_SCHEMA env
36
+ *
37
+ * Agent ref is intentionally NOT handled here — resolveIdentity (execution layer)
38
+ * covers it via getAgentConfig + resolveModel. Handling agent here would cause
39
+ * double-injection (agentConfig.systemPrompt at session-runner + appendSystemPrompt)
40
+ * and model-tier confusion.
38
41
  *
39
- * Returns the enriched opts and any temp files created (registered in activeTempFiles).
40
- * Caller is responsible for cleaning up files via cleanupAllTempFiles (session-scoped).
42
+ * Returns the enriched opts.
41
43
  */
42
- export function resolveAgentOpts(
43
- opts: AgentCallOpts,
44
- agentRegistry: AgentRegistry,
45
- sessionDir: string,
46
- activeTempFiles: Set<string>,
47
- ): ResolveResult {
48
- const systemPromptFiles: string[] = [];
49
-
50
- // Resolve agent system prompt
51
- if (opts.agent) {
52
- const discovered = agentRegistry.get(opts.agent); // 新 API: get() 替代 resolve(),返回 AgentConfig(含 systemPrompt+model)
53
- if (!discovered) {
54
- const available = agentRegistry.list().join(", ");
55
- return { opts, error: `Agent not found: ${opts.agent}. Available: ${available || "(none)"}` };
56
- }
57
-
58
- const hasSystemPrompt = discovered.systemPrompt.trim().length > 0;
59
- if (hasSystemPrompt) {
60
- try {
61
- const tmpDir = path.join(sessionDir, "workflow-tmp");
62
- fs.mkdirSync(tmpDir, { recursive: true });
63
- const tmpFile = path.join(tmpDir, `agent-prompt-${randomUUID()}.md`);
64
- fs.writeFileSync(tmpFile, discovered.systemPrompt, "utf-8");
65
- activeTempFiles.add(tmpFile);
66
- systemPromptFiles.push(tmpFile);
67
- } catch (err: unknown) {
68
- const msg = err instanceof Error ? err.message : String(err);
69
- return { opts, error: `Temp file write error: ${msg}` };
70
- }
71
- }
72
-
73
- // M3: 用 === undefined 而非 ||,避免空串被当 falsy 替换成 frontmatter model
74
- opts = {
75
- ...opts,
76
- model: opts.model === undefined ? discovered.model : opts.model,
77
- // M2: 传播 agent .md frontmatter 的 thinkingLevel(之前 AgentCallOpts 无此字段导致丢失)
78
- thinkingLevel: opts.thinkingLevel ?? discovered.thinkingLevel,
79
- };
80
- }
44
+ export function resolveAgentOpts(opts: AgentCallOpts): ResolveResult {
45
+ const appendSystemPrompt: string[] = [];
81
46
 
82
47
  // Resolve skill name to SKILL.md path
83
48
  if (opts.skill) {
@@ -88,53 +53,38 @@ export function resolveAgentOpts(
88
53
  opts = { ...opts, skillPath };
89
54
  }
90
55
 
91
- // Inject schema as structured-output instruction via --append-system-prompt
92
- // and set environment variable for conditional tool + hook activation.
56
+ // Inject schema as structured-output instruction into appendSystemPrompt (content,
57
+ // not temp file) and set environment variable for conditional tool + hook activation.
58
+ // M2 fix: previously wrote the instruction to a temp file and pushed the FILE PATH,
59
+ // which got concatenated into the final append file as path garbage — the SO instruction
60
+ // never reached the subprocess. Now the instruction content is pushed directly.
93
61
  if (opts.schema) {
94
- try {
95
- const tmpDir = path.join(sessionDir, "workflow-tmp");
96
- fs.mkdirSync(tmpDir, { recursive: true });
97
- const tmpFile = path.join(tmpDir, `so-${randomUUID().slice(0, UUID_SLICE_LEN)}.txt`);
98
- const schemaJson = JSON.stringify(opts.schema);
99
- const content = [
100
- "## MANDATORY: Structured Output Requirement",
101
- "",
102
- "This task requires structured output.",
103
- "Your FINAL action must be calling the `structured-output` tool.",
104
- "",
105
- "The schema is enforced by the system (PI_WORKFLOW_SCHEMA). You only pass `data` — do NOT pass a `schema` parameter.",
106
- `Your \`data\` must conform to this schema:`,
107
- "```json",
108
- schemaJson,
109
- "```",
110
- "",
111
- "Rules:",
112
- "- Call structured-output with ONLY the `data` parameter. The system validates it against the schema above automatically.",
113
- "- Do NOT output JSON in your text response — use the structured-output tool.",
114
- "- Do NOT skip this step. The structured-output call IS your result.",
115
- "- Complete all other work FIRST, then call structured-output as the last action.",
116
- ].join("\n");
117
- fs.writeFileSync(tmpFile, content, "utf-8");
118
- activeTempFiles.add(tmpFile);
119
- systemPromptFiles.push(tmpFile);
62
+ const schemaJson = JSON.stringify(opts.schema);
63
+ const content = [
64
+ "## MANDATORY: Structured Output Requirement",
65
+ "",
66
+ "This task requires structured output.",
67
+ "Your FINAL action must be calling the `structured-output` tool.",
68
+ "",
69
+ "The schema is enforced by the system (PI_WORKFLOW_SCHEMA). You only pass `data` — do NOT pass a `schema` parameter.",
70
+ `Your \`data\` must conform to this schema:`,
71
+ "```json",
72
+ schemaJson,
73
+ "```",
74
+ "",
75
+ "Rules:",
76
+ "- Call structured-output with ONLY the `data` parameter. The system validates it against the schema above automatically.",
77
+ "- Do NOT output JSON in your text response — use the structured-output tool.",
78
+ "- Do NOT skip this step. The structured-output call IS your result.",
79
+ "- Complete all other work FIRST, then call structured-output as the last action.",
80
+ ].join("\n");
81
+ appendSystemPrompt.push(content);
120
82
 
121
83
  // Set env var for structured-output extension to activate tool + hook
122
- opts = { ...opts, schemaEnv: schemaJson };
123
- } catch (err: unknown) {
124
- const msg = err instanceof Error ? err.message : String(err);
125
- return { opts, error: `Schema temp file write error: ${msg}` };
126
- }
84
+ opts = { ...opts, schemaEnv: schemaJson };
127
85
  }
128
86
 
129
87
  return {
130
- opts: { ...opts, ...(systemPromptFiles.length > 0 ? { systemPromptFiles } : {}) },
88
+ opts: { ...opts, ...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}) },
131
89
  };
132
90
  }
133
-
134
- /** Remove all remaining active temp files (called from session_shutdown). */
135
- export function cleanupAllTempFiles(activeTempFiles: Set<string>): void {
136
- for (const fp of activeTempFiles) {
137
- try { fs.unlinkSync(fp); } catch { /* already deleted */ void undefined; }
138
- }
139
- activeTempFiles.clear();
140
- }