@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
@@ -1,9 +1,16 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { describe, expect, it } from "vitest";
2
3
 
3
4
  import { CORE_PACKAGE_VERSION } from "../index";
4
5
 
6
+ // 对 package.json version 断言(单一事实源):本测试是 src/index.ts 字面量与
7
+ // package.json 双源同步的执法者——任何一侧 bump 后未同步另一侧立即红,
8
+ // 替代旧硬编码 "0.1.0" 字面量(版本 bump 时必忘改的第三处双源)。
5
9
  describe("subagent-core scaffold smoke", () => {
6
- it("exposes CORE_PACKAGE_VERSION", () => {
7
- expect(CORE_PACKAGE_VERSION).toBe("0.1.0");
10
+ it("exposes CORE_PACKAGE_VERSION synced with package.json", () => {
11
+ const pkgVersion = JSON.parse(
12
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
13
+ ).version;
14
+ expect(CORE_PACKAGE_VERSION).toBe(pkgVersion);
8
15
  });
9
16
  });
@@ -0,0 +1,232 @@
1
+ // src/execution/__tests__/agent-profile.test.ts
2
+ //
3
+ // parseAgentProfile 宽容解析测试(sink 设计 U2 / D3 / u-core-agent 验收①)。
4
+ //
5
+ // 字段形态覆盖矩阵(D3 定稿):单行 key:value、行内数组、block-scalar、
6
+ // 多行 `- item` 列表、无 frontmatter 宽容降级不抛;另含 legacy fallback
7
+ // (meta=null 单行兜底)与 parseResourceMeta 严格层新字段投影断言
8
+ // (AgentMeta 扩 maxTurns/disallowedTools/skills 的 typecheckMeta 行为)。
9
+
10
+ import { describe, expect, fail, it } from "vitest";
11
+
12
+ import { parseResourceMeta } from "../../shared/meta-parser.ts";
13
+ import { parseAgentProfile } from "../agent-registry.ts";
14
+
15
+ describe("parseAgentProfile(D3 宽容解析)", () => {
16
+ it("单行 key:value 全字段(IF1 通过)", () => {
17
+ const profile = parseAgentProfile(
18
+ `---
19
+ name: coder
20
+ description: coding agent
21
+ model: anthropic/claude-3.5-sonnet
22
+ thinkingLevel: high
23
+ tools: read, bash
24
+ maxTurns: 2
25
+ disallowedTools: write, edit
26
+ skills: code-review
27
+ ---
28
+ body text`,
29
+ "/x/coder.md",
30
+ );
31
+ expect(profile.name).toBe("coder");
32
+ expect(profile.description).toBe("coding agent");
33
+ expect(profile.body).toBe("body text");
34
+ expect(profile.model).toBe("anthropic/claude-3.5-sonnet");
35
+ expect(profile.thinkingLevel).toBe("high");
36
+ expect(profile.tools).toEqual(["read", "bash"]);
37
+ expect(profile.maxTurns).toBe(2);
38
+ expect(profile.disallowedTools).toEqual(["write", "edit"]);
39
+ expect(profile.skills).toEqual(["code-review"]);
40
+ expect(profile.meta).not.toBeNull();
41
+ expect(profile.warnings).toEqual([]);
42
+ });
43
+
44
+ it("行内数组形态(tools: [read, bash])", () => {
45
+ const profile = parseAgentProfile(
46
+ `---
47
+ name: coder
48
+ description: coding agent
49
+ tools: [read, bash]
50
+ ---
51
+ body`,
52
+ "/x/coder.md",
53
+ );
54
+ expect(profile.tools).toEqual(["read", "bash"]);
55
+ expect(profile.meta).not.toBeNull();
56
+ });
57
+
58
+ it("block-scalar description 完整保留(多行描述不截断)", () => {
59
+ const profile = parseAgentProfile(
60
+ `---
61
+ name: reviewer
62
+ description: |
63
+ first line of description
64
+ second line of description
65
+ ---
66
+ body`,
67
+ "/x/reviewer.md",
68
+ );
69
+ // `|` clip chomping 保留单个末尾换行(eemeli/yaml 标准行为)
70
+ expect(profile.description).toBe("first line of description\nsecond line of description\n");
71
+ expect(profile.meta?.description).toBe(profile.description);
72
+ });
73
+
74
+ it("多行 `- item` 列表形态(tools/disallowedTools/skills)", () => {
75
+ const profile = parseAgentProfile(
76
+ `---
77
+ name: builder
78
+ description: build agent
79
+ tools:
80
+ - read
81
+ - bash
82
+ disallowedTools:
83
+ - write
84
+ skills:
85
+ - code-review
86
+ - test-runner
87
+ ---
88
+ body`,
89
+ "/x/builder.md",
90
+ );
91
+ expect(profile.tools).toEqual(["read", "bash"]);
92
+ expect(profile.disallowedTools).toEqual(["write"]);
93
+ expect(profile.skills).toEqual(["code-review", "test-runner"]);
94
+ expect(profile.maxTurns).toBeUndefined();
95
+ });
96
+
97
+ it("无 frontmatter:宽容降级不抛——name=stem、description 空串、全文 body、无 warning", () => {
98
+ const profile = parseAgentProfile("# README\n\nplain markdown.", "/x/README.md");
99
+ expect(profile.name).toBe("README");
100
+ expect(profile.description).toBe("");
101
+ expect(profile.body).toBe("# README\n\nplain markdown.");
102
+ expect(profile.meta).toBeNull();
103
+ expect(profile.warnings).toEqual([]);
104
+ });
105
+
106
+ it("未闭合 frontmatter:name 经 legacy fallback、全文作 body、warning 可见", () => {
107
+ const text = `---\nname: broken\nmodel: x/y`;
108
+ const profile = parseAgentProfile(text, "/x/broken.md");
109
+ expect(profile.name).toBe("broken");
110
+ expect(profile.body).toBe(text);
111
+ expect(profile.meta).toBeNull();
112
+ expect(profile.warnings).toHaveLength(1);
113
+ expect(profile.warnings[0]).toContain("未闭合");
114
+ });
115
+
116
+ it("IF1 失败(缺 description):执行字段经单行 legacy fallback 不丢失 + warning", () => {
117
+ const profile = parseAgentProfile(
118
+ `---
119
+ name: legacy
120
+ model: anthropic/claude-3.5-sonnet
121
+ tools: read, bash
122
+ maxTurns: 5
123
+ ---
124
+ body text`,
125
+ "/x/legacy.md",
126
+ );
127
+ expect(profile.meta).toBeNull();
128
+ expect(profile.name).toBe("legacy");
129
+ expect(profile.description).toBe("");
130
+ expect(profile.model).toBe("anthropic/claude-3.5-sonnet");
131
+ expect(profile.tools).toEqual(["read", "bash"]);
132
+ // legacy fallback 只认字符串形态:`maxTurns: 5` 单行 → Number 换算
133
+ expect(profile.maxTurns).toBe(5);
134
+ expect(profile.warnings).toHaveLength(1);
135
+ expect(profile.warnings[0]).toContain("legacy fallback");
136
+ expect(profile.body).toBe("body text");
137
+ });
138
+
139
+ it("IF1 失败 + maxTurns 非数字:忽略该字段并追加 warning(不抛)", () => {
140
+ const profile = parseAgentProfile(
141
+ `---
142
+ name: weird
143
+ maxTurns: many
144
+ ---
145
+ body`,
146
+ "/x/weird.md",
147
+ );
148
+ expect(profile.meta).toBeNull();
149
+ expect(profile.maxTurns).toBeUndefined();
150
+ expect(profile.warnings.some((w) => w.includes("maxTurns"))).toBe(true);
151
+ });
152
+
153
+ it("name 缺省 stem(frontmatter 无 name 字段且 IF1 失败时)", () => {
154
+ const profile = parseAgentProfile("---\nmodel: x/y\n---\nbody", "/x/unnamed.md");
155
+ expect(profile.name).toBe("unnamed");
156
+ expect(profile.model).toBe("x/y");
157
+ });
158
+ });
159
+
160
+ describe("parseResourceMeta 严格层新字段投影(AgentMeta D3 扩展的 typecheckMeta 行为)", () => {
161
+ it("数字 maxTurns + 数组 disallowedTools/skills 投影进 AgentMeta", () => {
162
+ const meta = parseResourceMeta(
163
+ `---
164
+ name: coder
165
+ description: coding agent
166
+ maxTurns: 2
167
+ disallowedTools: [write]
168
+ skills:
169
+ - code-review
170
+ ---
171
+ body`,
172
+ "agent",
173
+ );
174
+ expect(meta).not.toBeNull();
175
+ if (meta?.kind !== "agent") return fail("kind 应为 agent");
176
+ expect(meta.maxTurns).toBe(2);
177
+ expect(meta.disallowedTools).toEqual(["write"]);
178
+ expect(meta.skills).toEqual(["code-review"]);
179
+ });
180
+
181
+ it("maxTurns 非有限数字 → reject(严格层不静默丢弃非法字段)", () => {
182
+ const meta = parseResourceMeta(
183
+ "---\nname: x\ndescription: y\nmaxTurns: many\n---\nbody",
184
+ "agent",
185
+ );
186
+ expect(meta).toBeNull();
187
+ });
188
+
189
+ it("disallowedTools 数组含非字符串 → reject", () => {
190
+ const meta = parseResourceMeta(
191
+ "---\nname: x\ndescription: y\ndisallowedTools: [write, 42]\n---\nbody",
192
+ "agent",
193
+ );
194
+ expect(meta).toBeNull();
195
+ });
196
+
197
+ it("disallowedTools/skills 空数组 → 键保留为 [](与 tools: [] 空列表语义一致)", () => {
198
+ const meta = parseResourceMeta(
199
+ "---\nname: x\ndescription: y\ntools: []\ndisallowedTools: []\nskills: []\n---\nbody",
200
+ "agent",
201
+ );
202
+ expect(meta).not.toBeNull();
203
+ if (meta?.kind !== "agent") return fail("kind 应为 agent");
204
+ expect("disallowedTools" in meta).toBe(true);
205
+ expect(meta.disallowedTools).toEqual([]);
206
+ expect("skills" in meta).toBe(true);
207
+ expect(meta.skills).toEqual([]);
208
+ // 同 fixture 的 tools 基准:空数组三字段族同语义
209
+ expect("tools" in meta).toBe(true);
210
+ expect(meta.tools).toEqual([]);
211
+ });
212
+
213
+ it("workflow meta 含 agent 专属新字段 → 串类 reject", () => {
214
+ const meta = parseResourceMeta(
215
+ "/* @pi-meta\nname: wf\ndescription: d\nphases: [a]\nmaxTurns: 2\n*/\ncode",
216
+ "workflow",
217
+ );
218
+ expect(meta).toBeNull();
219
+ });
220
+
221
+ it("缺 maxTurns/disallowedTools/skills 时 AgentMeta 不出现这些键(零回归)", () => {
222
+ const meta = parseResourceMeta(
223
+ "---\nname: x\ndescription: y\n---\nbody",
224
+ "agent",
225
+ );
226
+ expect(meta).not.toBeNull();
227
+ if (meta?.kind !== "agent") return fail("kind 应为 agent");
228
+ expect("maxTurns" in meta).toBe(false);
229
+ expect("disallowedTools" in meta).toBe(false);
230
+ expect("skills" in meta).toBe(false);
231
+ });
232
+ });
@@ -0,0 +1,219 @@
1
+ // src/execution/__tests__/agents-assembly.test.ts
2
+ //
3
+ // discoverAgents 装配函数测试(sink 设计 U2 / A6 / u-core-agent 验收②)。
4
+ //
5
+ // 等值对照口径:fixture 目录下,discoverAgents 产出与 pi 壳现装配循环
6
+ // (subagent-list-injector.discoverAllAgents)产出「同序同名同字段」。
7
+ // pi 壳装配循环无法直接 import 进 core 测试(core 对 pi-coding-agent /
8
+ // pi-extension-logger 零运行时触点——vitest.config 头注红线),故 oracle 在
9
+ // 测试内按 pi 循环逐句同构复刻,但全部消费 core 既有原语
10
+ // (discoverResources + parseResourceMeta 严格层 + sortByCodepoint)——
11
+ // 这正是 pi 循环消费的同一批 core 原语(pi 的 parseAgentFrontmatter 内部即
12
+ // parseResourceMeta(content, "agent") 投影)。oracle 与被测实现走不同解析路径
13
+ // (严格层 vs parseAgentProfile 宽容层),非自证。
14
+ //
15
+ // fixture 含全部清单可见性分支:正常条目(多形态)、同名遮蔽(hostRoots
16
+ // last-writer-wins)、无 frontmatter(README,两边都不进清单)、IF1 失败
17
+ // (缺 description,两边都不进清单)。
18
+
19
+ import * as fs from "node:fs";
20
+ import * as os from "node:os";
21
+ import * as path from "node:path";
22
+
23
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
24
+
25
+ import { configureCore, resetCoreForTests } from "../../core/host-services.ts";
26
+ import type { AgentEntry } from "../../shared/injection-render.ts";
27
+ import { sortByCodepoint } from "../../shared/injection-render.ts";
28
+ import { parseResourceMeta } from "../../shared/meta-parser.ts";
29
+ import {
30
+ clearFileCache,
31
+ discoverResources,
32
+ getCachedFileContent,
33
+ type DiscoveryRoot,
34
+ type ScanConfig,
35
+ } from "../../shared/resource-discovery.ts";
36
+ import { discoverAgents } from "../agents-assembly.ts";
37
+
38
+ // ── oracle:pi 壳 discoverAllAgents 装配循环的逐句同构(消费同一批 core 原语)──
39
+
40
+ async function piAssemblyOracle(
41
+ workspaceRoot: string,
42
+ hostRoots: DiscoveryRoot[],
43
+ ): Promise<AgentEntry[]> {
44
+ const config: ScanConfig = { kind: "agents", workspaceRoot, hostRoots };
45
+ const resources = await discoverResources(config);
46
+
47
+ const agentMap = new Map<string, AgentEntry>();
48
+ for (const resource of resources) {
49
+ if (!resource.available) continue;
50
+ const content = getCachedFileContent(resource.path);
51
+ if (content === null) continue; // pi: catch → logger.error → skip
52
+ // pi parseAgentFrontmatter 的投影面(parseResourceMeta 严格层)
53
+ const meta = parseResourceMeta(content, "agent");
54
+ if (meta && meta.kind === "agent") {
55
+ agentMap.set(meta.name, {
56
+ name: meta.name,
57
+ description: meta.description,
58
+ when: meta.when,
59
+ examples: meta.examples,
60
+ path: resource.path,
61
+ });
62
+ } else if (content.trimStart().startsWith("---")) {
63
+ // pi: startsWithFrontmatter → logger.warn(清单可见性口径:无 frontmatter 不 warn)
64
+ }
65
+ }
66
+ return sortByCodepoint([...agentMap.values()], (a) => a.name);
67
+ }
68
+
69
+ // ── fixture ──────────────────────────────────────────────────
70
+
71
+ function writeAgent(root: string, rel: string, content: string): string {
72
+ const abs = path.join(root, rel);
73
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
74
+ fs.writeFileSync(abs, content, "utf-8");
75
+ return abs;
76
+ }
77
+
78
+ describe("discoverAgents", () => {
79
+ let outerDir: string;
80
+ let workspaceRoot: string;
81
+ let hostRoot: string;
82
+ let logCalls: Array<{ level: string; component: string; message: string; data?: unknown }>;
83
+
84
+ beforeEach(() => {
85
+ outerDir = fs.mkdtempSync(path.join(os.tmpdir(), "agents-assembly-"));
86
+ workspaceRoot = path.join(outerDir, "ws");
87
+ hostRoot = path.join(outerDir, "host", "agents");
88
+ fs.mkdirSync(workspaceRoot, { recursive: true });
89
+ logCalls = [];
90
+ configureCore({
91
+ dataRoot: () => outerDir,
92
+ log: (level, component, message, data) => {
93
+ logCalls.push({ level, component, message, data });
94
+ },
95
+ });
96
+ });
97
+
98
+ afterEach(() => {
99
+ resetCoreForTests();
100
+ clearFileCache();
101
+ fs.rmSync(outerDir, { recursive: true, force: true });
102
+ });
103
+
104
+ it("与 pi 壳现装配循环产出等值(同序同名同字段)——多形态 fixture 对照", async () => {
105
+ // project-pi 源(workspaceRoot/.pi/agents)
106
+ writeAgent(
107
+ workspaceRoot,
108
+ ".pi/agents/alpha.md",
109
+ `---
110
+ name: alpha
111
+ description: single-line agent
112
+ model: provider/model-a
113
+ ---
114
+ alpha body`,
115
+ );
116
+ // block-scalar description + 多行 - item(zsw mini parser 全形态)
117
+ writeAgent(
118
+ workspaceRoot,
119
+ ".pi/agents/beta.md",
120
+ `---
121
+ name: beta
122
+ description: |
123
+ beta line one
124
+ beta line two
125
+ tools:
126
+ - read
127
+ - bash
128
+ ---
129
+ beta body`,
130
+ );
131
+ // 无 frontmatter(两边都不进清单、都不 warn)
132
+ writeAgent(workspaceRoot, ".pi/agents/README.md", "# docs\n");
133
+ // IF1 失败(缺 description,两边都不进清单、都 warn)
134
+ writeAgent(
135
+ workspaceRoot,
136
+ ".pi/agents/broken.md",
137
+ `---
138
+ name: broken
139
+ model: provider/x
140
+ ---
141
+ broken body`,
142
+ );
143
+ // hostRoots 注入源(序位高于 project-pi)——同名遮蔽 alpha + 新增 gamma
144
+ writeAgent(
145
+ hostRoot,
146
+ "alpha.md",
147
+ `---
148
+ name: alpha
149
+ description: host shadowing version
150
+ ---
151
+ host alpha body`,
152
+ );
153
+ writeAgent(
154
+ hostRoot,
155
+ "gamma.md",
156
+ `---
157
+ name: gamma
158
+ description: inline array agent
159
+ tools: [read, grep]
160
+ maxTurns: 2
161
+ ---
162
+ gamma body`,
163
+ );
164
+
165
+ const hostRoots: DiscoveryRoot[] = [{ dir: hostRoot, source: "project-host" }];
166
+ const [actual, oracle] = await Promise.all([
167
+ discoverAgents(workspaceRoot, hostRoots),
168
+ piAssemblyOracle(workspaceRoot, hostRoots),
169
+ ]);
170
+
171
+ // 同序同名同字段(逐条目投影对比,path 为绝对路径逐字节一致)
172
+ expect(actual).toEqual(oracle);
173
+ // 码点序:alpha < beta < gamma
174
+ expect(actual.map((a) => a.name)).toEqual(["alpha", "beta", "gamma"]);
175
+ // 同名遮蔽:alpha = hostRoots 胜出版(path + description 都是注入根的)
176
+ const alpha = actual.find((a) => a.name === "alpha");
177
+ expect(alpha?.path).toBe(path.join(hostRoot, "alpha.md"));
178
+ expect(alpha?.description).toBe("host shadowing version");
179
+ // 无 frontmatter / IF1 失败不进清单
180
+ expect(actual.some((a) => a.path.endsWith("README.md"))).toBe(false);
181
+ expect(actual.some((a) => a.path.endsWith("broken.md"))).toBe(false);
182
+ });
183
+
184
+ it("hostRoots 为空时仍扫硬编码槽(project .pi/agents),等值口径不破", async () => {
185
+ writeAgent(
186
+ workspaceRoot,
187
+ ".pi/agents/solo.md",
188
+ "---\nname: solo\ndescription: only one\n---\nbody",
189
+ );
190
+ const [actual, oracle] = await Promise.all([
191
+ discoverAgents(workspaceRoot, []),
192
+ piAssemblyOracle(workspaceRoot, []),
193
+ ]);
194
+ expect(actual).toEqual(oracle);
195
+ expect(actual.map((a) => a.name)).toEqual(["solo"]);
196
+ });
197
+
198
+ it("IF1 失败(有 frontmatter)→ warn 口径内聚:仅该类文件 warn,README 不 warn", async () => {
199
+ writeAgent(
200
+ workspaceRoot,
201
+ ".pi/agents/broken.md",
202
+ "---\nname: broken\nmodel: provider/x\n---\nbody",
203
+ );
204
+ writeAgent(workspaceRoot, ".pi/agents/README.md", "# plain doc\n");
205
+
206
+ const actual = await discoverAgents(workspaceRoot, []);
207
+ expect(actual).toEqual([]);
208
+
209
+ const warns = logCalls.filter((c) => c.level === "warn");
210
+ expect(warns).toHaveLength(1);
211
+ expect(warns[0]!.message).toContain("broken.md");
212
+ expect(warns[0]!.message).toContain("IF1 校验不通过");
213
+ });
214
+
215
+ it("空目录 / 不存在的 workspaceRoot:空清单不抛", async () => {
216
+ const actual = await discoverAgents(path.join(outerDir, "nonexistent"), []);
217
+ expect(actual).toEqual([]);
218
+ });
219
+ });
@@ -561,6 +561,77 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
561
561
  expect(h?.journalPath).toBe(resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId));
562
562
  });
563
563
 
564
+ // ============================================================
565
+ // [R4 §3.4 不变量 3] onHandleReady 运行中回填:create 应答后立即落 entry——
566
+ // 运行中 GUI 经 entry 重建 record 即得 ①②级读取钥匙(不等 run resolve)
567
+ // ============================================================
568
+
569
+ it("[onHandleReady] create 应答后回调 → record.engineHandle 立即回填 + reportRecordTransition 落 entry(record 仍 running)", async () => {
570
+ process.env.XYZ_AGENT_DATA_DIR = agentDir;
571
+ const { service, zcode, pi } = setup(agentDir);
572
+ const POOL = "zcode-appserver-home";
573
+ let releaseRun!: (v: { handle: EngineHandle; outcome: AgentOutcome }) => void;
574
+ zcode.runImpl = (task, ctx) => {
575
+ ctx.onPoolResolved?.(POOL);
576
+ // create 应答后的回调时点(app-server 引擎在 session/create 应答后触发)
577
+ ctx.onHandleReady?.({
578
+ sessionRef: { dbPath: ".zcode/cli/db/db.sqlite", sessionId: "sess-live-1" },
579
+ poolKey: POOL,
580
+ });
581
+ return new Promise((resolve) => {
582
+ releaseRun = resolve; // 挂起 run——模拟运行中任务
583
+ });
584
+ };
585
+ const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
586
+ await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
587
+
588
+ // run 尚未 resolve(record 仍 running):engineHandle 已回填且 entry 已落盘
589
+ await vi.waitFor(() => {
590
+ const entries = pi.appendEntry.mock.calls.filter((c) => c[0] === "subagent-record");
591
+ const withHandle = entries.filter(
592
+ (c) => (c[1] as Record<string, unknown>).engineHandle !== undefined,
593
+ );
594
+ expect(withHandle.length).toBeGreaterThan(0);
595
+ });
596
+ const running = service.collectRecords(10, "running").find((r) => r.id === handle.subagentId);
597
+ expect(running?.engineHandle).toEqual({
598
+ sessionRef: { dbPath: ".zcode/cli/db/db.sqlite", sessionId: "sess-live-1" },
599
+ poolKey: POOL,
600
+ journalPath: resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId),
601
+ });
602
+ // 回填 entry 的 journalPath 与 onPoolResolved retarget 后的实际落盘路径一致(同源)
603
+ expect(fs.existsSync(resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId))).toBe(false);
604
+
605
+ // 终态收口(防 dangling)
606
+ releaseRun({ handle: fakeHandle(), outcome: doneOutcome("ok") });
607
+ await vi.waitFor(() => expect(service.findRecord(handle.subagentId)).toBeUndefined());
608
+ }, 10_000);
609
+
610
+ it("[onHandleReady] 引擎不回调(spawn 形态)时零回填——终态回填仍兜底(行为不变)", async () => {
611
+ process.env.XYZ_AGENT_DATA_DIR = agentDir;
612
+ const { service, zcode, pi } = setup(agentDir);
613
+ const POOL = "zcode-p-glm";
614
+ zcode.runImpl = (task, ctx) => {
615
+ ctx.onPoolResolved?.(POOL); // 只 onPoolResolved——spawn 引擎形态(无 onHandleReady)
616
+ return Promise.resolve({
617
+ handle: {
618
+ data: { v: 1, engineId: "zcode", sessionRef: { dbPath: "sessions.db", sessionId: "sess-1" }, poolKey: POOL, adapterVersion: "test" },
619
+ },
620
+ outcome: doneOutcome("ok"),
621
+ });
622
+ };
623
+ const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
624
+ await vi.waitFor(() => expect(service.findRecord(handle.subagentId)).toBeUndefined());
625
+ const entries = pi.appendEntry.mock.calls.filter((c) => c[0] === "subagent-record");
626
+ // 运行中回填 entry 不存在(register 写点无 engineHandle;archive 终态侧才有)
627
+ const runningEntries = entries.slice(0, -1);
628
+ for (const c of runningEntries) {
629
+ expect((c[1] as Record<string, unknown>).engineHandle).toBeUndefined();
630
+ }
631
+ const final = entries[entries.length - 1][1] as Record<string, unknown>;
632
+ expect(final.engineHandle).toMatchObject({ poolKey: POOL });
633
+ });
634
+
564
635
  it("[D5 回归] pi 纯缺省路径 entry 不含 engine/engineFallback/engineHandle 键", async () => {
565
636
  const { service, pi } = setup(agentDir);
566
637
  const handle = await service.execute(baseOpts(agentDir));
@@ -58,6 +58,7 @@ vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
58
58
  // chatMode agent_end 早返回(continue),不读 session-pending;统一 count=0 防干扰。
59
59
  vi.mock("../session-pending.ts", () => ({
60
60
  readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
61
+ prunePendingCursor: vi.fn(),
61
62
  }));
62
63
 
63
64
  vi.mock("../temp-prompt.ts", () => ({