@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.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 (166) hide show
  1. package/README.md +15 -0
  2. package/package.json +7 -4
  3. package/scripts/rfl.mjs +308 -0
  4. package/skills/workflow-script-format/SKILL.md +1 -1
  5. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  6. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  7. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  8. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  9. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  10. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  11. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  12. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  13. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  14. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  15. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  16. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  17. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  18. package/src/execution/__tests__/execution-record.test.ts +199 -0
  19. package/src/execution/__tests__/finalize-record.test.ts +197 -15
  20. package/src/execution/__tests__/format.test.ts +131 -7
  21. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  22. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  23. package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
  24. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  25. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  26. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  27. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  28. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  29. package/src/execution/__tests__/list-component.test.ts +59 -5
  30. package/src/execution/__tests__/list-fields.test.ts +109 -0
  31. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  32. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  33. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  34. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  35. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  36. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  37. package/src/execution/__tests__/record-store.test.ts +442 -54
  38. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  39. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  40. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  41. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  42. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  43. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  44. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  45. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  46. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  47. package/src/execution/__tests__/session-pending.test.ts +61 -2
  48. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  49. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  50. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  51. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  52. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  53. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  54. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  55. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  56. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  57. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  59. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  60. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  61. package/src/execution/__tests__/tool-action.test.ts +12 -10
  62. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  63. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  64. package/src/execution/__tests__/worktree-manager.test.ts +300 -90
  65. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
  66. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  67. package/src/execution/__tests__/worktree-registry.test.ts +72 -34
  68. package/src/execution/agent-result-mapper.ts +4 -1
  69. package/src/execution/argv-mirror.ts +21 -2
  70. package/src/execution/channel-registry-access.ts +3 -1
  71. package/src/execution/execution-record.ts +126 -9
  72. package/src/execution/finalize-record.ts +99 -13
  73. package/src/execution/idle-gc.ts +47 -0
  74. package/src/execution/lifecycle-manager.ts +491 -0
  75. package/src/execution/lifecycle-predicates.ts +65 -0
  76. package/src/execution/manifest-store.ts +61 -16
  77. package/src/execution/model-resolver.ts +26 -5
  78. package/src/execution/notifier.ts +69 -12
  79. package/src/execution/pi-invocation.ts +21 -1
  80. package/src/execution/record-entry.ts +118 -0
  81. package/src/execution/record-store.ts +844 -108
  82. package/src/execution/session-pending.ts +121 -49
  83. package/src/execution/session-reconstructor.ts +224 -7
  84. package/src/execution/session-runner.ts +713 -316
  85. package/src/execution/sessions-index.ts +304 -0
  86. package/src/execution/stdin-writer.ts +93 -7
  87. package/src/execution/stream-sink.ts +20 -3
  88. package/src/execution/subagent-service.ts +917 -138
  89. package/src/execution/temp-prompt.ts +8 -3
  90. package/src/execution/turn-limiter.ts +14 -0
  91. package/src/execution/types.ts +218 -19
  92. package/src/execution/worktree-manager.ts +449 -59
  93. package/src/execution/worktree-registry.ts +97 -29
  94. package/src/index.ts +318 -20
  95. package/src/injectors/subagent-list-injector.ts +26 -8
  96. package/src/injectors/workflow-list-injector.ts +25 -8
  97. package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
  98. package/src/interface/__tests__/tool-render.test.ts +10 -8
  99. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
  100. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
  101. package/src/interface/bg-notify-render.ts +32 -8
  102. package/src/interface/command-actions.ts +26 -7
  103. package/src/interface/commands.ts +21 -22
  104. package/src/interface/format.ts +53 -20
  105. package/src/interface/gui-mappers.ts +6 -8
  106. package/src/interface/helpers.ts +170 -10
  107. package/src/interface/list-component.ts +53 -14
  108. package/src/interface/subagent-actions.ts +235 -17
  109. package/src/interface/subagent-tool.ts +81 -16
  110. package/src/interface/subagents.ts +2 -1
  111. package/src/interface/tool-render.ts +21 -27
  112. package/src/interface/tool-workflow-script.ts +29 -33
  113. package/src/interface/tool-workflow.ts +67 -100
  114. package/src/interface/views/WorkflowsView.ts +89 -32
  115. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  116. package/src/interface/views/detail-content.ts +1 -1
  117. package/src/interface/views/format.ts +3 -3
  118. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
  119. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  120. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  121. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  122. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  123. package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
  124. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  128. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  129. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
  130. package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
  131. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  132. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  133. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
  134. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  135. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  136. package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
  137. package/src/orchestration/agent-opts-resolver.ts +4 -1
  138. package/src/orchestration/args-validator.ts +2 -2
  139. package/src/orchestration/config-loader.ts +30 -1
  140. package/src/orchestration/error-recovery.ts +137 -30
  141. package/src/orchestration/execute-agent-call.ts +44 -10
  142. package/src/orchestration/jsonl-run-store.ts +397 -71
  143. package/src/orchestration/launcher.ts +7 -1
  144. package/src/orchestration/lifecycle.ts +145 -133
  145. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  146. package/src/orchestration/models/budget.ts +1 -1
  147. package/src/orchestration/models/run-runtime.ts +15 -17
  148. package/src/orchestration/models/run-spec.ts +2 -2
  149. package/src/orchestration/models/run-state.ts +3 -3
  150. package/src/orchestration/models/trace.ts +95 -15
  151. package/src/orchestration/models/types.ts +8 -9
  152. package/src/orchestration/models/workflow-run.ts +50 -71
  153. package/src/orchestration/models/workflow-script.ts +32 -1
  154. package/src/orchestration/skill-discovery.ts +30 -0
  155. package/src/orchestration/worker-handle.ts +1 -1
  156. package/src/orchestration/worker-host.ts +1 -1
  157. package/src/orchestration/worker-script-builder.ts +38 -11
  158. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  159. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  160. package/src/shared/__tests__/resource-discovery.test.ts +79 -0
  161. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  162. package/src/shared/agent-ref.ts +22 -1
  163. package/src/shared/resource-discovery.ts +162 -59
  164. package/src/shared/schema-jsonify.ts +56 -0
  165. package/workflows/review-fix-loop-utils.cjs +542 -32
  166. package/workflows/review-fix-loop.js +462 -109
@@ -0,0 +1,110 @@
1
+ /**
2
+ * buildWorkerScript 模板 hoisting(IF5/IF6)— byte-identical 快照锚定。
3
+ *
4
+ * ES8:快照先行——fixture 由「改造前实现」生成落盘(本文件同 commit),
5
+ * 断言重构后输出与 fixture 逐字节一致,锁定 PRE/POST 边界换行与 userScript
6
+ * 缩进语义(AC-4「逐字保留」不变式)。IF6(_KNOWN_FIELDS 提升至生成源
7
+ * module scope)落地时基线已按设计在同一 commit 内更新为最终形态。
8
+ *
9
+ * 样例脚本覆盖 $ARGS / schema / parallel / pipeline / workflow / phase / log 特性。
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
13
+ import { dirname, join } from "node:path";
14
+
15
+ import { describe, expect, it } from "vitest";
16
+
17
+ import { buildWorkerScript } from "../worker-script-builder.ts";
18
+
19
+ const sampleScript = [
20
+ "// sample exercising template features",
21
+ 'const name = $ARGS.name ?? "default";',
22
+ 'phase("build");',
23
+ 'const spec = await agent("build it", { label: "builder", schema: { type: "object" } });',
24
+ "const results = await parallel([",
25
+ ' agent({ task: "unit tests", agent: "./tester.md" }),',
26
+ ' agent({ task: "lint", agent: "./linter.md", skill: "lint-skill" }),',
27
+ "]);",
28
+ "await pipeline([(x) => x, (x) => x]);",
29
+ 'await workflow("deploy", { env: "prod" });',
30
+ 'log("done " + name + " " + $WORKSPACE + " " + $BUDGET.remaining());',
31
+ "module.exports = { execute: async (ctx) => ctx.agent(\"finalize\") };",
32
+ ].join("\n");
33
+
34
+ function loadFixture(): string {
35
+ const here = dirname(fileURLToPath(import.meta.url));
36
+ return readFileSync(join(here, "__fixtures__", "worker-template.snapshot.txt"), "utf8");
37
+ }
38
+
39
+ describe("buildWorkerScript — 模板 hoisting byte-identical 快照(IF5)", () => {
40
+ it("特性全量样例的输出与 fixture 逐字节一致", () => {
41
+ const out = buildWorkerScript(sampleScript);
42
+ expect(out).toBe(loadFixture());
43
+ });
44
+
45
+ it("首尾边界锚定:use strict 开头 + error handler 结尾 + userScript 缩进注入", () => {
46
+ const out = buildWorkerScript(sampleScript);
47
+ // PRE 边界:第一行 use strict(无前导换行)
48
+ expect(out.startsWith('"use strict";\n')).toBe(true);
49
+ // userScript 段:两空格缩进注入(AC-4 拼接语义)
50
+ expect(out).toContain("\n // sample exercising template features\n");
51
+ // userScript 段结束后一个空行再接 auto-invoke 段(POST 首部换行语义)
52
+ expect(out).toContain("ctx.agent(\"finalize\") };\n\n // ── Auto-invoke execute() for module.exports pattern ──");
53
+ // POST 边界:以 error handler 的 "});" 结尾(无尾随换行)
54
+ expect(out.endsWith('_safePost({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs }, "error");\n});')).toBe(true);
55
+ });
56
+
57
+ it("空 userScript 与任意脚本的拼接语义稳定(PRE/POST 不含 userScript 状态)", () => {
58
+ const a = buildWorkerScript("x");
59
+ const b = buildWorkerScript("y");
60
+ // 除 userScript 段外完全一致:以 userScript 行为界切割比对
61
+ const [preA] = a.split("\n x\n");
62
+ const [preB] = b.split("\n y\n");
63
+ expect(preA).toBe(preB);
64
+ });
65
+
66
+ it("A8 rfl 仪表透传字段字面量锚定(tier-1 §7.1):live resolve 与缓存重放两对称点均含三字段", () => {
67
+ const out = buildWorkerScript("// noop");
68
+ // live resolve 分支(agent-result 消息处理)
69
+ expect(out).toContain("usage: msg.result.usage,");
70
+ expect(out).toContain("durationMs: msg.result.durationMs,");
71
+ expect(out).toContain("sessionId: msg.result.sessionId,");
72
+ // 缓存重放分支(_callCache 命中重建,对称点)
73
+ expect(out).toContain("usage: cached.usage,");
74
+ expect(out).toContain("durationMs: cached.durationMs,");
75
+ expect(out).toContain("sessionId: cached.sessionId,");
76
+ });
77
+ });
78
+
79
+ describe("buildWorkerScript — _KNOWN_FIELDS module scope 提升(IF6)", () => {
80
+ const script = buildWorkerScript("// noop user script");
81
+
82
+ it("生成源含 module 级 _KNOWN_FIELDS 声明(与 _workerLogs 同级,IIFE 外)", () => {
83
+ // 声明在 IIFE 开始之前(module scope):const _KNOWN_FIELDS 必须出现在 "(async () => {" 前
84
+ const iifePos = script.indexOf("(async () => {");
85
+ const knownPos = script.indexOf("const _KNOWN_FIELDS = new Set([");
86
+ expect(knownPos).toBeGreaterThan(-1);
87
+ expect(knownPos).toBeLessThan(iifePos);
88
+ // 紧随 _workerLogs 声明之后(design IF6:与 _workerLogs/:62 同级)
89
+ expect(script.indexOf("const _workerLogs = [];")).toBeLessThan(knownPos);
90
+ });
91
+
92
+ it("agent() 体内不再重建 Set(引用 _KNOWN_FIELDS,无 new Set)", () => {
93
+ // agent() 函数体(到 parallel 声明前)内不得有 new Set 构造
94
+ const agentPos = script.indexOf("async function agent(firstArg, secondArg)");
95
+ const parallelPos = script.indexOf("async function parallel(calls)");
96
+ expect(agentPos).toBeGreaterThan(-1);
97
+ expect(parallelPos).toBeGreaterThan(agentPos);
98
+ const agentBody = script.slice(agentPos, parallelPos);
99
+ expect(agentBody).not.toContain("new Set(");
100
+ expect(agentBody).toContain("_KNOWN_FIELDS.has(k)");
101
+ });
102
+
103
+ it("字段集合内容逐字段一致(16 known fields 不丢失)", () => {
104
+ expect(script).toContain(
105
+ 'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);',
106
+ );
107
+ // unknown-fields 警告文案不变(known 列表仍全量)
108
+ expect(script).toMatch(/Known fields:.*returnMeta.*thinkingLevel/);
109
+ });
110
+ });
@@ -21,8 +21,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
21
21
  vi.mock("../lifecycle.ts", () => ({
22
22
  runWorkflow: vi.fn(),
23
23
  abortRun: vi.fn(async () => {}),
24
- pauseRun: vi.fn(async () => {}),
25
- resumeRun: vi.fn(async () => {}),
26
24
  scheduleTimeBudget: vi.fn(() => undefined),
27
25
  }));
28
26
 
@@ -0,0 +1,110 @@
1
+ /**
2
+ * WorkflowScript.validate lint memo(IF9/#15,TC9/DM2)— 引用相等键缓存测试。
3
+ *
4
+ * vi.mock script-lint 模块统计 lintScript 调用次数(design IF9 测试面):
5
+ * - 同 path + sourceCode 相等(含等值异字面量)→ 二次 validate 只 1 次 lint 调用
6
+ * - 【设计修正】design 原文「异引用等值 miss」在 JS 不可构造:字符串 === 是值相等
7
+ * (实测等值异字面量命中)。lintScript 是纯函数,值相等 ⟹ 结果相同,值键 memo
8
+ * 语义严格正确且是引用键意图的超集(registry 等值重建实例同样命中)。
9
+ * - 同 path 新内容(值不等)→ 重 lint 并更新条目
10
+ * - clearLintMemo → 重 lint(config-loader.invalidateCache 挂载点)
11
+ */
12
+ import { describe, it, expect, vi, beforeEach } from "vitest";
13
+
14
+ const lintCalls = vi.hoisted(() => ({ count: 0 }));
15
+
16
+ vi.mock("../script-lint.ts", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import("../script-lint.ts")>();
18
+ const realLintScript = actual.lintScript;
19
+ const countingLintScript = ((source: string) => {
20
+ lintCalls.count++;
21
+ return realLintScript(source);
22
+ }) as typeof actual.lintScript;
23
+ return { ...actual, lintScript: countingLintScript };
24
+ });
25
+
26
+ import { WorkflowScript, clearLintMemo, type WorkflowMeta } from "../models/workflow-script.ts";
27
+
28
+ const meta: WorkflowMeta = {
29
+ kind: "workflow",
30
+ name: "w",
31
+ description: "",
32
+ phases: [],
33
+ };
34
+
35
+ function makeScript(path: string, sourceCode: string): WorkflowScript {
36
+ return new WorkflowScript({
37
+ name: "w",
38
+ source: "saved",
39
+ path,
40
+ sourceCode,
41
+ meta,
42
+ available: true,
43
+ });
44
+ }
45
+
46
+ const VALID = '/* @pi-meta name: w */\nagent({ prompt: "x" });\n';
47
+
48
+ beforeEach(() => {
49
+ clearLintMemo();
50
+ lintCalls.count = 0;
51
+ });
52
+
53
+ describe("WorkflowScript.validate — lintMemo(IF9)", () => {
54
+ it("同 sourceCode 引用:二次 validate 只 1 次 lint 调用", () => {
55
+ const src = VALID;
56
+ const a = makeScript("/ws/.pi/workflows/w.js", src);
57
+ const b = makeScript("/ws/.pi/workflows/w.js", src); // registry 重建实例场景
58
+
59
+ const ra = a.validate();
60
+ const rb = b.validate();
61
+
62
+ expect(lintCalls.count).toBe(1);
63
+ expect(rb).toEqual(ra); // 缓存返回同一 lint 结果
64
+ });
65
+
66
+ it("等值异字面量同样命中(JS === 值相等;lintScript 纯函数 ⟹ 值键 memo 正确)", () => {
67
+ const a = makeScript("/ws/.pi/workflows/w.js", VALID);
68
+ const b = makeScript("/ws/.pi/workflows/w.js", `/* @pi-meta name: w */\nagent({ prompt: "x" });\n`);
69
+
70
+ a.validate();
71
+ b.validate();
72
+
73
+ // 设计原文预期「异引用 miss → 2 次」,但字符串 === 是值相等,等值必命中;
74
+ // lintScript(source) 纯函数,同值同结果,缓存正确性不受影响(实测锚定)。
75
+ expect(lintCalls.count).toBe(1);
76
+ });
77
+
78
+ it("同 path 新内容(值不等):重 lint 并更新条目", () => {
79
+ const path = "/ws/.pi/workflows/w.js";
80
+ const old = makeScript(path, VALID);
81
+ old.validate();
82
+ expect(lintCalls.count).toBe(1);
83
+
84
+ const edited = makeScript(path, '/* @pi-meta name: w */\nagent({ prompt: "y", outputSchema: {} });\n');
85
+ edited.validate();
86
+ expect(lintCalls.count).toBe(2); // 新引用 → miss → 重 lint
87
+
88
+ // 新条目已覆写:再以 edited 同引用 validate 命中缓存
89
+ const replay = makeScript(path, edited.sourceCode);
90
+ replay.validate();
91
+ expect(lintCalls.count).toBe(2);
92
+ });
93
+
94
+ it("clearLintMemo 后重 lint(invalidateCache 挂载点)", () => {
95
+ const a = makeScript("/ws/.pi/workflows/w.js", VALID);
96
+ a.validate();
97
+ expect(lintCalls.count).toBe(1);
98
+
99
+ clearLintMemo();
100
+ a.validate();
101
+ expect(lintCalls.count).toBe(2);
102
+ });
103
+
104
+ it("不同 path 互不共享条目", () => {
105
+ const src = VALID;
106
+ makeScript("/ws/.pi/workflows/a.js", src).validate();
107
+ makeScript("/ws/.pi/workflows/b.js", src).validate();
108
+ expect(lintCalls.count).toBe(2);
109
+ });
110
+ });
@@ -279,7 +279,7 @@ interface ScriptOutcome {
279
279
  /**
280
280
  * 断言 runAndWait 返回成功完成的终态。
281
281
  *
282
- * - result.status === "done"(runAndWait 恒 done)
282
+ * - result.status === "closed"(runAndWait 恒 done)
283
283
  * - result.reason === "completed"(脚本正常 return,非 failed/aborted/time_limited)
284
284
  * - result.scriptResult.status 是 "ok" 或 "partial"(脚本层 outcome,非 "error")
285
285
  * - result.error 为 undefined
@@ -476,63 +476,61 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
476
476
 
477
477
  it("TC4: actionRun 顺序——not_found 优先(平铺 + 不存在名);slug 护栏保留;chain 平铺 Correct 文案", async () => {
478
478
  const deps = makeDeps();
479
- // 平铺 + 不存在 name → not_found 优先(m6:registry.get 先于平铺检测)
480
- const notFound = await actionRun(
479
+ // 平铺 + 不存在 name → not_found 优先(m6:registry.get 先于平铺检测)。
480
+ // W4:not_found throw(pi 只对 execute throw 置 isError:true,返回值里的
481
+ // isError 被 agent-loop 丢弃)——文案含 <available_workflows> <location> 恢复指引,
482
+ // pi catch 后原样进 toolResult content。
483
+ const notFoundErr: Error = await actionRun(
481
484
  { action: "run", name: wf("nope"), task: "x" },
482
485
  deps,
483
486
  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(
487
+ ).catch((e: unknown) => e as Error);
488
+ expect(notFoundErr).toBeInstanceOf(Error);
489
+ expect(notFoundErr.message).toContain("not found"); // DoD 用户可见断言
490
+ expect(notFoundErr.message).toContain("<available_workflows>");
491
+ expect(notFoundErr.message).toContain("<location>");
492
+ // chain 平铺 task → throw Correct 正例(动态集来自 registry;W4b:原 textResult
493
+ // 间接 isError 返回改 throw——pi 只对 execute throw 置 isError:true,
494
+ // 返回值里的 isError agent-loop 丢弃)
495
+ const flatErr: Error = await actionRun(
492
496
  { action: "run", name: wf("chain"), task: "x" },
493
497
  deps,
494
498
  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(
499
+ ).catch((e: unknown) => e as Error);
500
+ expect(flatErr).toBeInstanceOf(Error);
501
+ expect(flatErr.message).toContain("Detected task at top level");
502
+ expect(flatErr.message).toContain("Correct:");
503
+ // slug 护栏保留(m6 顺序调整后仍在 runWorkflow 前;W4b 同改 throw)
504
+ const slugErr: Error = await actionRun(
501
505
  { action: "run", name: wf("chain"), args: { task: "x" }, slug: "a".repeat(40) },
502
506
  deps,
503
507
  undefined,
504
- );
505
- expect(slug.isError).toBe(true);
506
- expect(slug.content![0]!.text).toContain("slug exceeds");
508
+ ).catch((e: unknown) => e as Error);
509
+ expect(slugErr).toBeInstanceOf(Error);
510
+ expect(slugErr.message).toContain("slug exceeds");
507
511
  });
508
512
 
509
513
  it("TC6: 跨 workflow 平铺语义——review-fix-loop 平铺 task(非其参数)走 args-validator", async () => {
510
514
  const deps = makeDeps();
511
515
  // 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");
516
+ // → m3 chokepoint invalid_args(错误更准——评审 m-5 语义锁定)。
517
+ // W4:ArgsValidationError 直接 throw(message 原文即 §5.3 指引)
518
+ await expect(
519
+ actionRun({ action: "run", name: wf("review-fix-loop"), task: "x" }, deps, undefined),
520
+ ).rejects.toThrow(/Invalid args for workflow 'review-fix-loop'[\s\S]*targetType/);
521
521
  });
522
522
 
523
- it("TC9: actionRun 参数校验失败 → isError ToolResult + §5.3 指引", async () => {
523
+ it("TC9: actionRun 参数校验失败 → throw + §5.3 指引(W4)", async () => {
524
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,
525
+ await expect(
526
+ actionRun(
527
+ { action: "run", name: wf("review-fix-loop"), args: { batch1: agentMd("code-reviewer") } },
528
+ deps,
529
+ undefined,
530
+ ),
531
+ ).rejects.toThrow(
532
+ /Invalid args for workflow 'review-fix-loop'[\s\S]*targetType[\s\S]*Read the workflow script file/,
529
533
  );
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
534
  });
537
535
 
538
536
 
@@ -21,6 +21,7 @@
21
21
 
22
22
  import type { AgentCallOpts } from "./models/types.ts";
23
23
  import { resolveSkillPath } from "./skill-discovery.ts";
24
+ import { stringifySchemaCached } from "../shared/schema-jsonify.ts";
24
25
 
25
26
  export interface ResolveResult {
26
27
  opts: AgentCallOpts;
@@ -59,7 +60,9 @@ export function resolveAgentOpts(opts: AgentCallOpts): ResolveResult {
59
60
  // which got concatenated into the final append file as path garbage — the SO instruction
60
61
  // never reached the subprocess. Now the instruction content is pushed directly.
61
62
  if (opts.schema) {
62
- const schemaJson = JSON.stringify(opts.schema);
63
+ // IF7(#13):同 schema 对象引用的 compact stringify 走 WeakMap 缓存
64
+ // (与 session-runner formatSchemaInstruction 的 pretty 版共享缓存条目)
65
+ const schemaJson = stringifySchemaCached(opts.schema, "compact");
63
66
  const content = [
64
67
  "## MANDATORY: Structured Output Requirement",
65
68
  "",
@@ -62,8 +62,8 @@ function formatMessage(name: string, errors: readonly unknown[]): string {
62
62
  * 校验 spec.parameters(JSON Schema draft-07)对 spec.args 的约束。
63
63
  *
64
64
  * - spec.parameters === undefined → 跳过(安全退化:漏拷 parameters 退化是「不校验」非「校验错」)
65
- * - coerceTypes 原地规范化 spec.args(worker 启动 + pause/resume 重建共用同一对象,
66
- * run.spec === spec,保证恢复路径参数一致)
65
+ * - coerceTypes 原地规范化 spec.args(worker 启动 + 崩溃重建重跑共用同一对象,
66
+ * run.spec === spec,保证重跑路径参数一致)
67
67
  * - 失败 throw ArgsValidationError(非原始 ajv 错误)
68
68
  *
69
69
  * @throws ArgsValidationError 参数不合法或 schema 无效
@@ -16,6 +16,7 @@ import { resolve } from "node:path";
16
16
 
17
17
  // WorkflowMeta 规范来源是 shared/resource-meta.ts(m1 DM1);WorkflowSource 来自 workflow-script
18
18
  import type { WorkflowSource } from "./models/workflow-script.ts";
19
+ import { clearLintMemo } from "./models/workflow-script.ts";
19
20
  import type { WorkflowMeta } from "../shared/resource-meta.ts";
20
21
  import { getCachedFile, getCachedFileContent, clearFileCache } from "../shared/resource-discovery.ts";
21
22
  import { parseResourceMeta } from "../shared/meta-parser.ts";
@@ -93,6 +94,20 @@ function toWorkflowSource(source: ResourceSource): WorkflowSource {
93
94
  return source === "project-pi-tmp" ? "tmp" : "saved";
94
95
  }
95
96
 
97
+ /**
98
+ * 按路径推导 ResourceSource(IF4/外部 #5 残留)。
99
+ *
100
+ * getWorkflowByPath 可加载任意绝对路径(不限扫描源),此前硬编码 "user-pi"
101
+ * 使 workspaceRoot/.pi/workflows/.tmp/ 下的临时脚本按路径加载被错标 saved
102
+ * (discoverWorkflows 对同文件正确标 tmp,两路径不一致)。修正:路径落在
103
+ * tmp 目录前缀内 → "project-pi-tmp",其余 → "user-pi"(非 tmp 路径输出不变,DS4)。
104
+ */
105
+ function deriveResourceSource(filePath: string, workspaceRoot: string): ResourceSource {
106
+ const tmpDir = resolve(workspaceRoot, ".pi/workflows/.tmp");
107
+ const normalized = resolve(filePath);
108
+ return normalized === tmpDir || normalized.startsWith(`${tmpDir}/`) ? "project-pi-tmp" : "user-pi";
109
+ }
110
+
96
111
  // ── 单文件 → CachedWorkflowMeta(IF1 parseResourceMeta + 整对象透传)──
97
112
 
98
113
  /**
@@ -264,11 +279,23 @@ export async function getWorkflow(name: string): Promise<CachedWorkflowMeta | un
264
279
  * - ~/ 前缀展开;相对路径/非 .js 引用返回 undefined(引用唯一形态 = 绝对路径)
265
280
  * - 任意路径(不限扫描源):内置包内脚本、用户任意位置脚本均可执行
266
281
  * - meta 提取失败/文件不可读 → available=false(fail-safe,不抛)
282
+ * - [perf] 与 getWorkflow(name) 对称走 bucket 缓存(key=绝对路径,与 stem 名不冲突),
283
+ * 消除 workflow tool 主路径每次 run 的全文 regex + YAML.parse(mtime 判变失效)
267
284
  */
268
285
  export async function getWorkflowByPath(ref: string): Promise<CachedWorkflowMeta | undefined> {
269
286
  const filePath = normalizeRef(ref, WORKFLOW_REF_EXT);
270
287
  if (filePath === null) return undefined;
271
- return toCachedMeta(filePath, "user-pi");
288
+ const workspaceRoot = findWorkspaceRoot();
289
+ const bucket = getCacheBucket(workspaceRoot);
290
+ const cached = bucket.get(filePath);
291
+ if (cached && isCacheValid(cached)) {
292
+ return cached.meta;
293
+ }
294
+ // IF4:source 按路径推导——.tmp 前缀 → project-pi-tmp(→ "tmp"),其余 user-pi 维持现值
295
+ const meta = await toCachedMeta(filePath, deriveResourceSource(filePath, workspaceRoot));
296
+ const file = getCachedFile(filePath);
297
+ if (file) bucket.set(filePath, { meta, mtimeMs: file.mtimeMs });
298
+ return meta;
272
299
  }
273
300
 
274
301
  /**
@@ -277,6 +304,8 @@ export async function getWorkflowByPath(ref: string): Promise<CachedWorkflowMeta
277
304
  */
278
305
  export function invalidateCache(): void {
279
306
  // m5:清统一 mtime 缓存层 + bucket(测试隔离 + mtime 漏判场景手动刷新兜底)
307
+ // IF9:连带清 workflow-script validate 的 lint memo(同源文件缓存,测试隔离统一走它)
280
308
  clearFileCache();
281
309
  cache.clear();
310
+ clearLintMemo();
282
311
  }