@zhushanwen/subagent-core 0.2.0 → 0.4.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 (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -0,0 +1,474 @@
1
+ // src/shared/__tests__/resource-discovery-host-roots.test.ts
2
+ //
3
+ // C2-core-discovery(convergence W2)单元测试:发现链扩面四工作项的行为锁定。
4
+ // 覆盖:async 链 realpath 去重(W2①)、project-host 槽位(W2②)、hostRoots 同标签
5
+ // 多根语义(W2④,Map→列表 + 硬编码槽合并原根后置)、单层扫描维持(红线 3)、
6
+ // pi 单条目形态回归快照(回归红线:Map→列表是行为敏感改动)。
7
+ // 设计权威源:docs/design/subagent-core-convergence.md §3.2 D-2 / §3.3 红线 1-4。
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+
14
+ // 隔离真实用户全局目录(~/.agents/agents/)——与 resource-discovery.test.ts 同款
15
+ // 手法:mock homedir 到真实 tmpdir 子目录(macOS SIP 禁止 mkdir /nonexistent-*)。
16
+ const mockHomeDir = vi.hoisted(() => {
17
+ const fs = require("node:fs");
18
+ const os = require("node:os");
19
+ const path = require("node:path");
20
+ return fs.mkdtempSync(path.join(os.tmpdir(), "res-disc-hr-home-"));
21
+ });
22
+
23
+ vi.mock("node:os", async (importOriginal) => {
24
+ const actual = await importOriginal<typeof import("node:os")>();
25
+ return { ...actual, homedir: () => mockHomeDir };
26
+ });
27
+
28
+ import { discoverResources } from "../resource-discovery.ts";
29
+ // logger spy 断言(facade 同 component 单例引用,spyOn 即拦截模块内 logger 调用)
30
+ import { getLogger } from "../../core/logger.ts";
31
+ import type { DiscoveryRoot } from "../../core/host-services.ts";
32
+
33
+ // ============================================================
34
+ // helpers(与 resource-discovery.test.ts 同构)
35
+ // ============================================================
36
+
37
+ function tmpWorkspace(): string {
38
+ return fs.mkdtempSync(path.join(os.tmpdir(), "res-disc-hr-"));
39
+ }
40
+
41
+ function writeFile(dir: string, name: string, content: string): string {
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ const filePath = path.join(dir, name);
44
+ fs.writeFileSync(filePath, content, "utf-8");
45
+ return filePath;
46
+ }
47
+
48
+ function writePackageJson(pkgDir: string, pi?: Record<string, unknown>): void {
49
+ fs.mkdirSync(pkgDir, { recursive: true });
50
+ fs.writeFileSync(
51
+ path.join(pkgDir, "package.json"),
52
+ JSON.stringify({ name: "test-pkg", ...(pi ? { pi } : {}) }),
53
+ "utf-8",
54
+ );
55
+ }
56
+
57
+ /** pi 壳单条目形态三根(agentDirKindRoots 同构):user-pi / npm / npm-dev。 */
58
+ function piHostRoots(agentDir: string, kind: "agents" | "workflows"): DiscoveryRoot[] {
59
+ return [
60
+ { dir: path.join(agentDir, kind), source: "user-pi" },
61
+ { dir: path.join(agentDir, "npm", "node_modules"), source: "npm" },
62
+ { dir: path.join(agentDir, "extensions"), source: "npm-dev" },
63
+ ];
64
+ }
65
+
66
+ // ============================================================
67
+ // W2①: async 链 realpath 去重(多链同文件)
68
+ // ============================================================
69
+
70
+ describe("C2 W2①: async 链 realpath 去重", () => {
71
+ let ws: string;
72
+
73
+ beforeEach(() => {
74
+ ws = tmpWorkspace();
75
+ });
76
+ afterEach(() => {
77
+ fs.rmSync(ws, { recursive: true, force: true });
78
+ });
79
+
80
+ it("多链同文件(a.md 本体 + b.md symlink → a.md)清单只留 1 条", async () => {
81
+ const agentDir = path.join(ws, ".fake-agent");
82
+ const dir = path.join(agentDir, "agents");
83
+ const realFile = writeFile(dir, "a.md", "shared-body");
84
+ fs.symlinkSync(realFile, path.join(dir, "b.md"));
85
+
86
+ const result = await discoverResources({
87
+ kind: "agents",
88
+ workspaceRoot: ws,
89
+ hostRoots: piHostRoots(agentDir, "agents"),
90
+ });
91
+
92
+ // stem 去重(a/b 不同 stem)防不住多链同文件——realpath 归一后只留首遇者 a.md
93
+ expect(result).toHaveLength(1);
94
+ expect(result[0]?.path).toBe(realFile);
95
+ expect(result[0]?.source).toBe("user-pi");
96
+ });
97
+
98
+ it("多链同文件不是遮蔽——不产生 duplicate 遮蔽报告(warn/debug 均无)", async () => {
99
+ const agentDir = path.join(ws, ".fake-agent");
100
+ const dir = path.join(agentDir, "agents");
101
+ const realFile = writeFile(dir, "a.md", "shared-body");
102
+ fs.symlinkSync(realFile, path.join(dir, "b.md"));
103
+
104
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
105
+ const debugSpy = vi.spyOn(getLogger("subagents"), "debug");
106
+ try {
107
+ await discoverResources({
108
+ kind: "agents",
109
+ workspaceRoot: ws,
110
+ hostRoots: piHostRoots(agentDir, "agents"),
111
+ });
112
+ expect(warnSpy).not.toHaveBeenCalled();
113
+ expect(debugSpy).not.toHaveBeenCalled();
114
+ } finally {
115
+ warnSpy.mockRestore();
116
+ debugSpy.mockRestore();
117
+ }
118
+ });
119
+
120
+ it("跨源多链同文件同样归一(user-pi 链 + user-agents 硬编码根链 → 1 条)", async () => {
121
+ const agentDir = path.join(ws, ".fake-agent");
122
+ // 物理文件在 ws 库目录;user-pi 根与 user-agents 硬编码根各挂一个不同名 symlink
123
+ const libFile = writeFile(path.join(ws, "lib"), "origin.md", "lib-body");
124
+ const userPiLink = path.join(agentDir, "agents", "alpha.md");
125
+ fs.mkdirSync(path.dirname(userPiLink), { recursive: true });
126
+ fs.symlinkSync(libFile, userPiLink);
127
+ const userAgentsLink = path.join(mockHomeDir, ".agents", "agents", "beta.md");
128
+ fs.mkdirSync(path.dirname(userAgentsLink), { recursive: true });
129
+ fs.symlinkSync(libFile, userAgentsLink);
130
+
131
+ const result = await discoverResources({
132
+ kind: "agents",
133
+ workspaceRoot: ws,
134
+ hostRoots: piHostRoots(agentDir, "agents"),
135
+ });
136
+
137
+ expect(result).toHaveLength(1);
138
+ expect(result[0]?.path).toBe(userPiLink); // 首遇者(user-pi 序位在前)
139
+ expect(result[0]?.source).toBe("user-pi");
140
+ // 清理 mockHome 内 fixture
141
+ fs.rmSync(userAgentsLink, { force: true });
142
+ });
143
+
144
+ it("同 stem 不同物理文件仍按 last-writer-wins 覆盖(realpath 去重不伤遮蔽语义)", async () => {
145
+ const agentDir = path.join(ws, ".fake-agent");
146
+ writeFile(path.join(agentDir, "agents"), "clash.md", "user-pi-body");
147
+ writeFile(path.join(ws, ".agents", "agents"), "clash.md", "project-body");
148
+
149
+ const result = await discoverResources({
150
+ kind: "agents",
151
+ workspaceRoot: ws,
152
+ hostRoots: piHostRoots(agentDir, "agents"),
153
+ });
154
+
155
+ expect(result).toHaveLength(1);
156
+ expect(result[0]?.source).toBe("project-agents"); // 高优先级覆盖,语义不变
157
+ expect(result[0]?.path).toBe(path.join(ws, ".agents", "agents", "clash.md"));
158
+ });
159
+ });
160
+
161
+ // ============================================================
162
+ // W2④: hostRoots 同标签多根语义(Map→列表)
163
+ // ============================================================
164
+
165
+ describe("C2 W2④: hostRoots 同标签多根语义", () => {
166
+ let ws: string;
167
+ let agentDir: string;
168
+
169
+ beforeEach(() => {
170
+ ws = tmpWorkspace();
171
+ agentDir = path.join(ws, ".fake-agent");
172
+ });
173
+ afterEach(() => {
174
+ fs.rmSync(ws, { recursive: true, force: true });
175
+ });
176
+
177
+ it("同标签两条 user-pi 根都被扫描(非靠后者整体覆盖)", async () => {
178
+ const root1 = path.join(ws, "multi-root-a");
179
+ const root2 = path.join(ws, "multi-root-b");
180
+ writeFile(root1, "one.md", "one");
181
+ writeFile(root2, "two.md", "two");
182
+
183
+ const result = await discoverResources({
184
+ kind: "agents",
185
+ workspaceRoot: ws,
186
+ hostRoots: [
187
+ { dir: root1, source: "user-pi" },
188
+ { dir: root2, source: "user-pi" },
189
+ { dir: path.join(agentDir, "npm", "node_modules"), source: "npm" },
190
+ { dir: path.join(agentDir, "extensions"), source: "npm-dev" },
191
+ ],
192
+ });
193
+
194
+ // 旧 Map 语义:root2 覆盖 root1 → one.md 消失;列表语义:两根同序位依次扫描
195
+ const userPi = result.filter((r) => r.source === "user-pi");
196
+ expect(userPi.map((r) => path.basename(r.path)).sort()).toEqual(["one.md", "two.md"]);
197
+ });
198
+
199
+ it("同标签多根 + 同 stem 撞名:后注入的本体根胜(红线 2 本体在后)", async () => {
200
+ // 宿主(zsw)注入形态:目录 symlink 展开目标在前、本体根在后
201
+ const expanded = path.join(ws, "expanded-from-lib");
202
+ const own = path.join(ws, "own-agents");
203
+ writeFile(expanded, "shared.md", "lib-body");
204
+ writeFile(own, "shared.md", "own-body");
205
+
206
+ const result = await discoverResources({
207
+ kind: "agents",
208
+ workspaceRoot: ws,
209
+ hostRoots: [
210
+ { dir: expanded, source: "user-pi" },
211
+ { dir: own, source: "user-pi" },
212
+ { dir: path.join(agentDir, "npm", "node_modules"), source: "npm" },
213
+ { dir: path.join(agentDir, "extensions"), source: "npm-dev" },
214
+ ],
215
+ });
216
+
217
+ expect(result).toHaveLength(1);
218
+ // core 合并 last-writer-wins(靠后者胜)→ 本体根注入在后 = 本体胜
219
+ expect(result[0]?.path).toBe(path.join(own, "shared.md"));
220
+ expect(result[0]?.source).toBe("user-pi");
221
+ });
222
+
223
+ it("user-agents 标签注入与硬编码根合并(硬编码本体根在后 = 硬编码胜)", async () => {
224
+ const injected = path.join(ws, "inj-agents");
225
+ writeFile(injected, "h.md", "injected-body");
226
+ const hardFile = writeFile(path.join(mockHomeDir, ".agents", "agents"), "h.md", "hard-body");
227
+
228
+ try {
229
+ const result = await discoverResources({
230
+ kind: "agents",
231
+ workspaceRoot: ws,
232
+ hostRoots: [
233
+ { dir: injected, source: "user-agents" },
234
+ { dir: path.join(agentDir, "npm", "node_modules"), source: "npm" },
235
+ { dir: path.join(agentDir, "extensions"), source: "npm-dev" },
236
+ ],
237
+ });
238
+
239
+ expect(result).toHaveLength(1);
240
+ // 注入条目在前、硬编码根(本体)在后——last-writer-wins 落硬编码
241
+ expect(result[0]?.path).toBe(hardFile);
242
+ expect(result[0]?.source).toBe("user-agents");
243
+ } finally {
244
+ fs.rmSync(path.join(mockHomeDir, ".agents", "agents", "h.md"), { force: true });
245
+ }
246
+ });
247
+
248
+ it("project-agents 标签注入与硬编码根合并(硬编码本体根在后 = 硬编码胜)", async () => {
249
+ const injected = path.join(ws, "inj-proj-agents");
250
+ writeFile(injected, "p.md", "injected-body");
251
+ const hardFile = writeFile(path.join(ws, ".agents", "agents"), "p.md", "hard-body");
252
+
253
+ const result = await discoverResources({
254
+ kind: "agents",
255
+ workspaceRoot: ws,
256
+ hostRoots: [
257
+ { dir: injected, source: "project-agents" },
258
+ { dir: path.join(agentDir, "npm", "node_modules"), source: "npm" },
259
+ { dir: path.join(agentDir, "extensions"), source: "npm-dev" },
260
+ ],
261
+ });
262
+
263
+ expect(result).toHaveLength(1);
264
+ expect(result[0]?.path).toBe(hardFile);
265
+ expect(result[0]?.source).toBe("project-agents");
266
+ });
267
+
268
+ it("project-host 标签同标签多根依注入序生效(project-host 支持列表语义)", async () => {
269
+ const r1 = path.join(ws, "ph-root-a");
270
+ const r2 = path.join(ws, "ph-root-b");
271
+ writeFile(r1, "ph1.md", "one");
272
+ writeFile(r2, "ph2.md", "two");
273
+
274
+ const result = await discoverResources({
275
+ kind: "agents",
276
+ workspaceRoot: ws,
277
+ hostRoots: [
278
+ { dir: r1, source: "project-host" },
279
+ { dir: r2, source: "project-host" },
280
+ ],
281
+ });
282
+
283
+ const ph = result.filter((r) => r.source === "project-host");
284
+ expect(ph.map((r) => path.basename(r.path)).sort()).toEqual(["ph1.md", "ph2.md"]);
285
+ });
286
+ });
287
+
288
+ // ============================================================
289
+ // W2②: project-host 槽位序位
290
+ // ============================================================
291
+
292
+ describe("C2 W2②: project-host 槽位序位", () => {
293
+ let ws: string;
294
+ let agentDir: string;
295
+ let hostRoot: string;
296
+
297
+ beforeEach(() => {
298
+ ws = tmpWorkspace();
299
+ agentDir = path.join(ws, ".fake-agent");
300
+ hostRoot = path.join(ws, ".zcode", "agents");
301
+ });
302
+ afterEach(() => {
303
+ fs.rmSync(ws, { recursive: true, force: true });
304
+ });
305
+
306
+ it("project-host 同名被 project-agents 遮蔽(project-agents 是项目级最高逃生门)", async () => {
307
+ writeFile(hostRoot, "clash.md", "host-body");
308
+ writeFile(path.join(ws, ".agents", "agents"), "clash.md", "proj-body");
309
+
310
+ const result = await discoverResources({
311
+ kind: "agents",
312
+ workspaceRoot: ws,
313
+ hostRoots: [
314
+ ...piHostRoots(agentDir, "agents"),
315
+ { dir: hostRoot, source: "project-host" },
316
+ ],
317
+ });
318
+
319
+ const clash = result.find((r) => path.basename(r.path) === "clash.md");
320
+ expect(clash?.source).toBe("project-agents");
321
+ expect(clash?.path).toBe(path.join(ws, ".agents", "agents", "clash.md"));
322
+ });
323
+
324
+ it("project-host 压过 project-pi(序位:project-pi < project-pi-tmp < project-host)", async () => {
325
+ writeFile(path.join(ws, ".pi", "agents"), "ph.md", "pi-body");
326
+ writeFile(hostRoot, "ph.md", "host-body");
327
+
328
+ const result = await discoverResources({
329
+ kind: "agents",
330
+ workspaceRoot: ws,
331
+ hostRoots: [
332
+ ...piHostRoots(agentDir, "agents"),
333
+ { dir: hostRoot, source: "project-host" },
334
+ ],
335
+ });
336
+
337
+ const ph = result.find((r) => path.basename(r.path) === "ph.md");
338
+ expect(ph?.source).toBe("project-host");
339
+ expect(ph?.path).toBe(path.join(hostRoot, "ph.md"));
340
+ });
341
+
342
+ it("宿主未注入 project-host 标签时槽位缺席(输出无 project-host 条目)", async () => {
343
+ writeFile(path.join(ws, ".pi", "agents"), "only.md", "body");
344
+ const result = await discoverResources({
345
+ kind: "agents",
346
+ workspaceRoot: ws,
347
+ hostRoots: piHostRoots(agentDir, "agents"),
348
+ });
349
+ expect(result.filter((r) => r.source === "project-host")).toEqual([]);
350
+ expect(result.map((r) => path.basename(r.path))).toEqual(["only.md"]);
351
+ });
352
+ });
353
+
354
+ // ============================================================
355
+ // 红线 3: 单层扫描维持(不递归 / node_modules 不灌清单)
356
+ // ============================================================
357
+
358
+ describe("C2 红线 3: 单层扫描维持", () => {
359
+ let ws: string;
360
+ let agentDir: string;
361
+
362
+ beforeEach(() => {
363
+ ws = tmpWorkspace();
364
+ agentDir = path.join(ws, ".fake-agent");
365
+ });
366
+ afterEach(() => {
367
+ fs.rmSync(ws, { recursive: true, force: true });
368
+ });
369
+
370
+ it("子目录不递归(nested/deep.md 不入清单)", async () => {
371
+ writeFile(path.join(ws, ".pi", "agents", "nested"), "deep.md", "body");
372
+ writeFile(path.join(ws, ".pi", "agents"), "top.md", "body");
373
+
374
+ const result = await discoverResources({
375
+ kind: "agents",
376
+ workspaceRoot: ws,
377
+ hostRoots: piHostRoots(agentDir, "agents"),
378
+ });
379
+
380
+ const project = result.filter((r) => r.source === "project-pi");
381
+ expect(project.map((r) => path.basename(r.path))).toEqual(["top.md"]);
382
+ });
383
+
384
+ it("源目录内 node_modules 目录在单层语义下不灌清单", async () => {
385
+ // user-pi 根内的 node_modules/ 是目录(非 .md 文件)——单层扫描不展开
386
+ writeFile(path.join(agentDir, "agents", "node_modules", "some-pkg"), "pkg-agent.md", "body");
387
+ writeFile(path.join(agentDir, "agents"), "real.md", "body");
388
+
389
+ const result = await discoverResources({
390
+ kind: "agents",
391
+ workspaceRoot: ws,
392
+ hostRoots: piHostRoots(agentDir, "agents"),
393
+ });
394
+
395
+ const userPi = result.filter((r) => r.source === "user-pi");
396
+ expect(userPi.map((r) => path.basename(r.path))).toEqual(["real.md"]);
397
+ });
398
+ });
399
+
400
+ // ============================================================
401
+ // 回归红线: pi 单条目形态快照(Map→列表行为敏感改动的锁定)
402
+ // ============================================================
403
+
404
+ describe("C2 回归红线: pi 单条目形态快照", () => {
405
+ let ws: string;
406
+ let agentDir: string;
407
+
408
+ beforeEach(() => {
409
+ ws = tmpWorkspace();
410
+ agentDir = path.join(ws, ".fake-agent");
411
+ });
412
+ afterEach(() => {
413
+ fs.rmSync(ws, { recursive: true, force: true });
414
+ });
415
+
416
+ it("与 pi 真实 agentDir 同构 fixture——source 标签与胜出路径逐项稳定", async () => {
417
+ // fixture 同构本机真实 agentDir(probe-c2 探针 2026-08-30 输出形态):
418
+ // user-pi 三文件 / npm 包 10 内置角色(pi.agents manifest)/ user-agents 撞名
419
+ // tech-design-review / npm-dev 一包 / project 两源为空
420
+ writeFile(path.join(agentDir, "agents"), "general-purpose.md", "user-pi-gp");
421
+ writeFile(path.join(agentDir, "agents"), "tech-design-review.md", "user-pi-tdr");
422
+ writeFile(path.join(agentDir, "agents"), "vision-analyze.md", "user-pi-va");
423
+ writeFile(path.join(mockHomeDir, ".agents", "agents"), "tech-design-review.md", "user-agents-tdr");
424
+
425
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "@zhushanwen", "pi-subagent-workflow");
426
+ writePackageJson(npmPkg, { agents: ["./agents"] });
427
+ for (const role of [
428
+ "general-purpose", "analyst", "coder", "debugger", "doc-reviewer",
429
+ "explorer", "orchestrator", "planner", "researcher", "reviewer",
430
+ ]) {
431
+ writeFile(path.join(npmPkg, "agents"), `${role}.md`, `npm-${role}`);
432
+ }
433
+
434
+ const devPkg = path.join(agentDir, "extensions", "local-dev-pkg");
435
+ writePackageJson(devPkg); // 无 pi manifest → 约定目录
436
+ writeFile(path.join(devPkg, "agents"), "dev-extra.md", "npm-dev-body");
437
+
438
+ try {
439
+ const result = await discoverResources({
440
+ kind: "agents",
441
+ workspaceRoot: ws,
442
+ hostRoots: piHostRoots(agentDir, "agents"),
443
+ });
444
+
445
+ // 逐项断言(序 = stem 首插序,值 = last-writer-wins 胜出者):
446
+ // 前 3 个 stem 首插于 user-pi(真实探针同构),general-purpose 被 npm 覆盖、
447
+ // tech-design-review 被 user-agents 覆盖(胜出 source/路径随之变化、位置不动)
448
+ expect(
449
+ result.map((r) => ({ source: r.source, base: path.basename(r.path) })),
450
+ ).toEqual([
451
+ { source: "npm", base: "general-purpose.md" },
452
+ { source: "user-agents", base: "tech-design-review.md" },
453
+ { source: "user-pi", base: "vision-analyze.md" },
454
+ { source: "npm", base: "analyst.md" },
455
+ { source: "npm", base: "coder.md" },
456
+ { source: "npm", base: "debugger.md" },
457
+ { source: "npm", base: "doc-reviewer.md" },
458
+ { source: "npm", base: "explorer.md" },
459
+ { source: "npm", base: "orchestrator.md" },
460
+ { source: "npm", base: "planner.md" },
461
+ { source: "npm", base: "researcher.md" },
462
+ { source: "npm", base: "reviewer.md" },
463
+ { source: "npm-dev", base: "dev-extra.md" },
464
+ ]);
465
+ // 胜出路径逐项锚定(绝对路径,非仅 basename)
466
+ expect(result[0]?.path).toBe(path.join(npmPkg, "agents", "general-purpose.md"));
467
+ expect(result[1]?.path).toBe(path.join(mockHomeDir, ".agents", "agents", "tech-design-review.md"));
468
+ expect(result[2]?.path).toBe(path.join(agentDir, "agents", "vision-analyze.md"));
469
+ expect(result[12]?.path).toBe(path.join(devPkg, "agents", "dev-extra.md"));
470
+ } finally {
471
+ fs.rmSync(path.join(mockHomeDir, ".agents", "agents", "tech-design-review.md"), { force: true });
472
+ }
473
+ });
474
+ });
@@ -561,9 +561,10 @@ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
561
561
  }
562
562
  });
563
563
 
564
- it("分级穷举:全部 8 个 ResourceSource 的机器/用户归属与 D3 一致", () => {
564
+ it("分级穷举:全部 9 个 ResourceSource 的机器/用户归属与 D3 一致", () => {
565
565
  // 封闭枚举逐值断言,防止未来新增/修改枚举值时分级边界漂移
566
- const machine: ResourceSource[] = ["npm", "npm-dev", "user-extension-paths", "project-pi", "project-pi-tmp", "project-agents"];
566
+ // (W2② 新增 project-host——项目级宿主注入根,机器源)
567
+ const machine: ResourceSource[] = ["npm", "npm-dev", "user-extension-paths", "project-pi", "project-pi-tmp", "project-host", "project-agents"];
567
568
  const user: ResourceSource[] = ["user-pi", "user-agents"];
568
569
  for (const s of machine) expect(isMachineSource(s), `${s} 应为机器源`).toBe(true);
569
570
  for (const s of user) expect(isMachineSource(s), `${s} 应为用户源`).toBe(false);
@@ -8,6 +8,13 @@
8
8
  *
9
9
  * 名字不再是引用——只是注入段展示标签。执行侧拿到引用后一律 normalizeRef →
10
10
  * 文件加载(AgentRegistry.loadByPath / WorkflowScriptRegistry.getPath)。
11
+ *
12
+ * `..` 路径段拒绝(sink 设计 ⛔2 声明的安全收紧,agent .md 与 workflow .js 引用
13
+ * 同受此面——两宿主现状均放行,非等值变更):`/x/../y.md` 形态允许引用逃逸出
14
+ * 注入段声明的目录集(G1 例 2),引用串含 `..` 段一律拒绝。检测在 `~/` 展开前的
15
+ * 原始引用上——join 展开会 normalize 消解 `..` 段(`~/../x` → `<home>/../x` 被解析
16
+ * 成已消解路径),原始串检测使 `~/../x.md` 与 `/x/../y.md` 同判(`..` 逃逸向量与
17
+ * 前缀形态无关)。保留 `.` 段与 `~/` 展开——最小收紧,不伤合法引用。
11
18
  */
12
19
  import { homedir } from "node:os";
13
20
  import { isAbsolute, join } from "node:path";
@@ -16,15 +23,25 @@ import { isAbsolute, join } from "node:path";
16
23
  const HOME_DIR_PREFIX = "~/";
17
24
 
18
25
  /**
19
- * 归一化资源引用:~ 展开 + 绝对路径校验。
26
+ * 引用串是否含 `..` 段(`/x/../y`、`x\..\y`、`~/../x` 同判——展开前检测)。
27
+ * 手动 split(/[\\/]) 与 displayAgentName 同款跨平台切分(本模块避免平台分支)。
28
+ */
29
+ function hasParentSegment(ref: string): boolean {
30
+ return ref.split(/[\\/]/).includes("..");
31
+ }
32
+
33
+ /**
34
+ * 归一化资源引用:~ 展开 + 绝对路径校验 + `..` 段拒绝(⛔2 安全收紧)。
20
35
  *
21
36
  * @param ref 原始引用(注入段 location / 工具参数值)
22
37
  * @param ext 期望扩展名(如 ".md" / ".js"),不匹配返回 null
23
- * @returns 归一化绝对路径;非法(空/相对路径/扩展名不符)返回 null
38
+ * @returns 归一化绝对路径;非法(空/相对路径/含 `..` 段/扩展名不符)返回 null
24
39
  */
25
40
  export function normalizeRef(ref: string, ext?: string): string | null {
26
41
  const trimmed = ref.trim();
27
42
  if (!trimmed) return null;
43
+ // ⛔2:`..` 段 = 目录逃逸向量,展开前检测(join 会 normalize 消解 `..`,展开后判会漏)
44
+ if (hasParentSegment(trimmed)) return null;
28
45
 
29
46
  const expanded = trimmed.startsWith(HOME_DIR_PREFIX)
30
47
  ? join(homedir(), trimmed.slice(HOME_DIR_PREFIX.length))
@@ -35,6 +52,130 @@ export function normalizeRef(ref: string, ext?: string): string | null {
35
52
  return expanded;
36
53
  }
37
54
 
55
+ // ── 报错文案工厂(sink 设计 U1)──────────────────────────────
56
+
57
+ /** invalidAgentRefMessage 的可选注入:宿主的清单段名(缺省 pi 的注入段)。 */
58
+ export interface InvalidAgentRefMessageOptions {
59
+ /**
60
+ * 清单指引:宿主注入段名。缺省 `<available_subagents>`(pi 注入段);第三宿主
61
+ * 注入段名不同(zsw `<available_agents>` 等)时注入替代,文案口径不变。
62
+ */
63
+ howToList?: string;
64
+ }
65
+
66
+ /**
67
+ * agent ref 非法的统一报错文案(消费方唯一文案口径——不各自拼 message)。
68
+ *
69
+ * 基准 = AgentRegistry.loadByPath(require) 的既有 throw 文案
70
+ * ("Invalid agent ref: ... absolute paths to .md files ..."),工厂收敛后该消费点
71
+ * 与后续宿主接线点共用此处。`..` 形态附纠正指引(⛔2 新失败路径——错误必须指向
72
+ * 恢复动作:去掉 `..` 段、改用注入段 location)。
73
+ */
74
+ export function invalidAgentRefMessage(
75
+ ref: string,
76
+ opts?: InvalidAgentRefMessageOptions,
77
+ ): string {
78
+ const howToList = opts?.howToList ?? "<available_subagents>";
79
+ if (hasParentSegment(ref)) {
80
+ return (
81
+ `Invalid agent ref: ${ref}. Agent refs must be absolute paths to .md files ` +
82
+ `without ".." path segments (use <location> from ${howToList}).`
83
+ );
84
+ }
85
+ return (
86
+ `Invalid agent ref: ${ref}. Agent refs must be absolute paths to .md files ` +
87
+ `(use <location> from ${howToList}).`
88
+ );
89
+ }
90
+
91
+ // ── workflow ref 原语(sink 设计 U1 / §2.2 D3 行)────────────
92
+
93
+ /** normalizeWorkflowRef 的可选注入:宿主已知的 workflow 名清单。 */
94
+ export interface NormalizeWorkflowRefOptions {
95
+ /**
96
+ * 宿主注入的已知 workflow 名清单(内置 + 用户级,宿主按自身优先级合并去重——
97
+ * pi 为 config-loader 的 tmp>project>npm>user 去重产物,内置名优先已由该清单
98
+ * 体现)。省略 = 无已知名,全部裸名按 unknown_name 拒绝。
99
+ */
100
+ knownNames?: Iterable<string>;
101
+ }
102
+
103
+ /** workflow ref 名分支的保留字:`.` / `..` 是文件系统相对路径段保留语义,任何域不可作名引用。 */
104
+ export const WORKFLOW_REF_RESERVED_NAMES: readonly string[] = [".", ".."];
105
+
106
+ /** normalizeWorkflowRef 拒绝原因(结构化裁决——文案留宿主,与 knownNames 宿主注入哲学一致)。 */
107
+ export type WorkflowRefInvalidReason =
108
+ | "empty" // 空引用
109
+ | "reserved" // 名分支命中保留字(保留字裁决优先于 knownNames)
110
+ | "unknown_name" // 裸名未命中 knownNames
111
+ | "not_absolute" // 路径分支:相对路径无基准
112
+ | "parent_segment" // 路径分支:含 `..` 段(⛔2 同面)
113
+ | "bad_ext"; // 路径分支:非 .js
114
+
115
+ /**
116
+ * normalizeWorkflowRef 的三分裁决结果:
117
+ * - name → 裸名命中 knownNames,按名引用(内置名优先已由宿主清单体现)
118
+ * - path → 路径形态经 normalizeRef 全套校验(~/ 展开、绝对路径、.js、`..` 拒绝)
119
+ * - invalid → 拒绝(reason 给精确裁决原因,消费方出恢复指引)
120
+ */
121
+ export type NormalizedWorkflowRef =
122
+ | { kind: "name"; name: string }
123
+ | { kind: "path"; path: string }
124
+ | { kind: "invalid"; ref: string; reason: WorkflowRefInvalidReason };
125
+
126
+ /**
127
+ * workflow 引用统一原语(名/路径二分 + 保留字裁决 + 内置名优先,三层口径单源化——
128
+ * pi「收裸名+路径」、zsw「入口拒裸名/内层宽松」的统一替代)。
129
+ *
130
+ * 二分判据:含 `/`、`\` 分隔符或 `~` 前缀 → 路径分支;否则 → 裸名分支。对齐 pi
131
+ * 现行为「名字是简单标识符,路径串不会撞 workflow 名」(tool-workflow actionRun:
132
+ * get(name) 先于 getPath——名命中即用,不猜路径,内置名优先由此成立)。
133
+ *
134
+ * 裁决顺序(裸名分支):保留字 > knownNames > unknown——保留字命中即拒,即便宿主
135
+ * 清单异常含保留字也不放行(`..` 若被当名接受会绕过路径域的 ⛔2 收紧)。
136
+ *
137
+ * 路径分支复用 normalizeRef(WORKFLOW_REF_EXT):`..` 段拒绝、~/ 展开、扩展名校验
138
+ * 与 agent 域同面(G1:同一校验语义)。
139
+ */
140
+ export function normalizeWorkflowRef(
141
+ ref: string,
142
+ opts?: NormalizeWorkflowRefOptions,
143
+ ): NormalizedWorkflowRef {
144
+ const trimmed = ref.trim();
145
+ if (!trimmed) return { kind: "invalid", ref, reason: "empty" };
146
+
147
+ const looksLikePath = trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("~");
148
+ if (looksLikePath) {
149
+ const path = normalizeRef(trimmed, WORKFLOW_REF_EXT);
150
+ if (path !== null) return { kind: "path", path };
151
+ // 失败原因细分(normalizeRef 返回 null 不带原因——此处按同一判据重放;重放
152
+ // 顺序与 normalizeRef 判序逐位对齐:parent_segment 先于 not_absolute,否则
153
+ // `a/../b.js` 类「相对路径 + 含 .. 段」被误归因 not_absolute,指引改绝对路径
154
+ // 后仍被 .. 段拒绝——两步误导。`..` 段对原始串检测,与展开前判位一致)
155
+ const expanded = trimmed.startsWith(HOME_DIR_PREFIX)
156
+ ? join(homedir(), trimmed.slice(HOME_DIR_PREFIX.length))
157
+ : trimmed;
158
+ const reason: WorkflowRefInvalidReason = hasParentSegment(trimmed)
159
+ ? "parent_segment"
160
+ : !isAbsolute(expanded)
161
+ ? "not_absolute"
162
+ : "bad_ext";
163
+ return { kind: "invalid", ref, reason };
164
+ }
165
+
166
+ // 裸名分支:保留字 > knownNames > unknown
167
+ if (WORKFLOW_REF_RESERVED_NAMES.includes(trimmed)) {
168
+ return { kind: "invalid", ref, reason: "reserved" };
169
+ }
170
+ const known = opts?.knownNames;
171
+ if (known !== undefined) {
172
+ for (const name of known) {
173
+ if (name === trimmed) return { kind: "name", name: trimmed };
174
+ }
175
+ }
176
+ return { kind: "invalid", ref, reason: "unknown_name" };
177
+ }
178
+
38
179
  /** agentRef 扩展名。 */
39
180
  export const AGENT_REF_EXT = ".md";
40
181
  /** workflowRef 扩展名。 */