@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
@@ -53,6 +53,12 @@ interface ErrorMsg {
53
53
  error: string;
54
54
  workerLogs?: unknown[];
55
55
  }
56
+ /** log 消息:脚本 log() 全局发出的独立诊断消息(协议见 worker-script-builder 头注释)。 */
57
+ interface LogMsg {
58
+ type: "log";
59
+ phase?: string;
60
+ message?: string;
61
+ }
56
62
 
57
63
  // ── 类型守卫:从 unknown 收窄到判别联合 ──
58
64
  // 共享 hasType 辅助:避免每个守卫重复 `(m as {type?:string})` 断言(taste/no-unsafe-catch)。
@@ -74,16 +80,23 @@ function isReturn(m: unknown): m is ReturnMsg {
74
80
  function isError(m: unknown): m is ErrorMsg {
75
81
  return hasType(m, "error");
76
82
  }
83
+ function isLog(m: unknown): m is LogMsg {
84
+ return hasType(m, "log");
85
+ }
77
86
 
78
87
  // ── 测试辅助:起一个真实 Worker 跑 buildWorkerScript 产物 ──────────────
79
88
 
80
89
  interface RunResult {
81
90
  /** 收到的 return 消息的 result 字段(脚本正常结束时)。 */
82
91
  returnValue?: unknown;
92
+ /** return 消息带回的 workerLogs([B-3] 起只含 console.* 捕获条目,log() 不再走此通路)。 */
93
+ returnWorkerLogs?: unknown[];
83
94
  /** 收到的 error 消息的 error 字段(脚本 throw 时)。 */
84
95
  errorMessage?: string;
85
96
  /** error 消息带回的 workerLogs(验证诊断不丢)。 */
86
97
  errorWorkerLogs?: unknown[];
98
+ /** 收到的 {type:"log"} 独立消息列表([B-3] 起 log() 的唯一通路)。 */
99
+ logMessages: LogMsg[];
87
100
  /** Worker exit code(0=正常,1=崩溃)。 */
88
101
  exitCode?: number;
89
102
  /** Worker 'error' 事件的错误消息(uncaught exception,正常应为 undefined)。 */
@@ -94,6 +107,15 @@ interface RunResult {
94
107
  workflowCalls: WorkflowCallMsg[];
95
108
  }
96
109
 
110
+ /** workerLogs 条目守卫收窄 + message 包含判定(元素类型 unknown,断言前先收窄)。 */
111
+ function someLogMessageContains(logs: unknown[] | undefined, text: string): boolean {
112
+ return (logs ?? []).some((l) => {
113
+ if (typeof l !== "object" || l === null) return false;
114
+ const message = (l as { message?: unknown }).message;
115
+ return typeof message === "string" && message.includes(text);
116
+ });
117
+ }
118
+
97
119
  interface RunOptions {
98
120
  /** $ARGS。 */
99
121
  args?: Record<string, unknown>;
@@ -144,7 +166,7 @@ function runWorker(userScript: string, opts: RunOptions = {}): Promise<RunResult
144
166
  // S8:创建后立即登记,afterEach 兜底清理(防止 promise 泄漏导致 Worker 未终止)
145
167
  createdWorkers.push(worker);
146
168
 
147
- const result: RunResult = { agentCalls: [], workflowCalls: [] };
169
+ const result: RunResult = { agentCalls: [], workflowCalls: [], logMessages: [] };
148
170
  let agentCallIdx = 0;
149
171
  let resolved = false;
150
172
  const timer = setTimeout(() => {
@@ -181,8 +203,13 @@ function runWorker(userScript: string, opts: RunOptions = {}): Promise<RunResult
181
203
  result.workflowCalls.push(raw);
182
204
  const wfResult = opts.handleWorkflowCall ? opts.handleWorkflowCall(raw) : { ok: true };
183
205
  worker.postMessage({ type: "workflow-result", callId: raw.callId, result: wfResult });
206
+ } else if (isLog(raw)) {
207
+ // [OR-6/T7④] 独立 log 消息收集(主线程真实 runtime 当前无消费 case,测试
208
+ // harness 收集以断言「协议消息仍发出」通路不被回退)。
209
+ result.logMessages.push(raw);
184
210
  } else if (isReturn(raw)) {
185
211
  result.returnValue = raw.result;
212
+ result.returnWorkerLogs = raw.workerLogs;
186
213
  finish(result);
187
214
  } else if (isError(raw)) {
188
215
  result.errorMessage = raw.error;
@@ -382,6 +409,56 @@ describe("buildWorkerScript runtime — 之前缺失的路径覆盖", () => {
382
409
  });
383
410
  });
384
411
 
412
+ // ── [B-3] log() 单通路可观测性 ─────────────────────────────────────
413
+ // 旧双通路(log() 同时 post 独立消息 + 记入 _workerLogs 随 return/error 带回)让
414
+ // 同一日志在主线程 errorLogs 占两格、TUI 双份。修复后 log() 只发独立 {type:"log"}
415
+ // 消息,主线程 handleWorkerLog 即时消费(error-recovery.ts);崩溃场景丢 log 条目
416
+ // 可接受(log 是 T7 补充可观测)。
417
+
418
+ describe("buildWorkerScript runtime — [B-3] log() 单通路", () => {
419
+ it("log() 发出独立 {type:\"log\"} 消息且携带 phase(主线程即时消费的唯一通路)", async () => {
420
+ const script = `
421
+ phase("build");
422
+ log("hello from script");
423
+ return { ok: true };
424
+ `;
425
+ const res = await runWorker(script);
426
+ expect(res.workerError).toBeUndefined();
427
+ expect(res.logMessages).toEqual([{ type: "log", phase: "build", message: "hello from script" }]);
428
+ expect(res.exitCode).not.toBe(1);
429
+ });
430
+
431
+ it("log() 内容不再随 return 消息的 workerLogs 带回(双份入账已退役)", async () => {
432
+ const script = `
433
+ phase("build");
434
+ log("hello from script");
435
+ return { ok: true };
436
+ `;
437
+ const res = await runWorker(script);
438
+ expect(res.workerError).toBeUndefined();
439
+ // workerLogs 中不得出现 log() 的条目(否则主线程 log case + workerLogs 双份入账)
440
+ expect(someLogMessageContains(res.returnWorkerLogs, "hello from script")).toBe(false);
441
+ });
442
+
443
+ it("log() 内容不再随 error 消息的 workerLogs 带回(console.* 捕获通路不受影响)", async () => {
444
+ const script = `
445
+ log("before-crash");
446
+ console.error("crash-context");
447
+ throw new Error("boom");
448
+ `;
449
+ const res = await runWorker(script);
450
+ expect(res.errorMessage).toBe("boom");
451
+ // log() 的条目不在 error workerLogs(单通路)
452
+ expect(someLogMessageContains(res.errorWorkerLogs, "before-crash")).toBe(false);
453
+ // console.* 捕获通路保持(workerLogs 仍带回 console.error 条目)
454
+ expect(res.errorWorkerLogs).toEqual(
455
+ expect.arrayContaining([
456
+ expect.objectContaining({ level: "error", message: "crash-context" }),
457
+ ]),
458
+ );
459
+ });
460
+ });
461
+
385
462
  // ── W2: agent() returnMeta 模式运行时验证 ───────────────────────────
386
463
  // 现有 worker-script-builder.test.ts 的 16 条 returnMeta 断言全是字符串 toContain,
387
464
  // 无法验证「真实 Worker 线程执行时 agent({returnMeta:true}) resolve 出对象、
@@ -0,0 +1,186 @@
1
+ // src/orchestration/__tests__/workflow-files.test.ts
2
+ //
3
+ // C4-core-script-pipeline(convergence W4 / D-6)目录参数化测试:
4
+ // - saveWorkflow/deleteWorkflow 注入非 .pi 目录:真实落盘 / rename 迁移 / 删除;
5
+ // - 既有语义回归(文案逐字):tmp 不存在拒、目标已存在拒、运行中拒、
6
+ // 两候选路径均无拒;
7
+ // - 缺省目录 = pi 布局(相对 cwd resolve,pi 现两参调用形态行为不变)。
8
+ // 设计权威源:docs/design/subagent-core-convergence.md §3.2 D-6。
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+
13
+ import { describe, expect, it, vi } from "vitest";
14
+
15
+ import { deleteWorkflow, saveWorkflow } from "../workflow-files.ts";
16
+
17
+ /** 构造注入布局(非 .pi):{root}/host-tmp + {root}/host-saved(saved 不预创建,
18
+ * 覆盖 saveWorkflow 内 mkdirSync recursive 自动建目录路径)。 */
19
+ function makeHostLayout(): { root: string; tmpDir: string; savedDir: string } {
20
+ const root = mkdtempSync(join(tmpdir(), "subagent-core-wf-"));
21
+ const tmpDir = join(root, "host-tmp");
22
+ const savedDir = join(root, "host-saved");
23
+ mkdirSync(tmpDir, { recursive: true });
24
+ return { root, tmpDir, savedDir };
25
+ }
26
+
27
+ function seedTmp(tmpDir: string, name: string, body = "// tmp body"): void {
28
+ writeFileSync(join(tmpDir, `${name}.js`), body);
29
+ }
30
+
31
+ // ============================================================
32
+ // saveWorkflow(目录注入)
33
+ // ============================================================
34
+
35
+ describe("saveWorkflow 目录注入(非 .pi 布局)", () => {
36
+ it("注入 tmpDir/savedDir:tmp 落盘 → rename 迁移,savedDir 自动创建", async () => {
37
+ const { root, tmpDir, savedDir } = makeHostLayout();
38
+ try {
39
+ seedTmp(tmpDir, "alpha", "// alpha body");
40
+ const msg = await saveWorkflow("alpha", undefined, { tmpDir, savedDir });
41
+ expect(msg).toBe(`Saved 'alpha' → 'alpha' (${join(savedDir, "alpha.js")})`);
42
+ // rename 语义:tmp 文件保存后消失
43
+ expect(existsSync(join(tmpDir, "alpha.js"))).toBe(false);
44
+ expect(readFileSync(join(savedDir, "alpha.js"), "utf-8")).toBe("// alpha body");
45
+ } finally {
46
+ rmSync(root, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ it("newName 改名:tmp/alpha.js → saved/beta.js", async () => {
51
+ const { root, tmpDir, savedDir } = makeHostLayout();
52
+ try {
53
+ seedTmp(tmpDir, "alpha", "// renamed body");
54
+ const msg = await saveWorkflow("alpha", "beta", { tmpDir, savedDir });
55
+ expect(msg).toBe(`Saved 'alpha' → 'beta' (${join(savedDir, "beta.js")})`);
56
+ expect(existsSync(join(savedDir, "alpha.js"))).toBe(false);
57
+ expect(existsSync(join(savedDir, "beta.js"))).toBe(true);
58
+ } finally {
59
+ rmSync(root, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ it("目标已存在 → 拒(文案逐字)", async () => {
64
+ const { root, tmpDir, savedDir } = makeHostLayout();
65
+ try {
66
+ seedTmp(tmpDir, "alpha");
67
+ mkdirSync(savedDir, { recursive: true });
68
+ writeFileSync(join(savedDir, "beta.js"), "// existing");
69
+ await expect(saveWorkflow("alpha", "beta", { tmpDir, savedDir })).rejects.toThrow(
70
+ "'beta' already exists in saved workflows. Use a different name.",
71
+ );
72
+ // 原文件不动
73
+ expect(existsSync(join(tmpDir, "alpha.js"))).toBe(true);
74
+ } finally {
75
+ rmSync(root, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ it("tmp 不存在 → 拒(文案逐字)", async () => {
80
+ const { root, tmpDir, savedDir } = makeHostLayout();
81
+ try {
82
+ await expect(saveWorkflow("ghost", undefined, { tmpDir, savedDir })).rejects.toThrow(
83
+ "Temporary workflow 'ghost' not found",
84
+ );
85
+ } finally {
86
+ rmSync(root, { recursive: true, force: true });
87
+ }
88
+ });
89
+ });
90
+
91
+ // ============================================================
92
+ // deleteWorkflow(目录注入)
93
+ // ============================================================
94
+
95
+ describe("deleteWorkflow 目录注入(非 .pi 布局)", () => {
96
+ it("注入目录:tmp 候选优先删除", () => {
97
+ const { root, tmpDir, savedDir } = makeHostLayout();
98
+ try {
99
+ seedTmp(tmpDir, "dup", "// tmp copy");
100
+ mkdirSync(savedDir, { recursive: true });
101
+ writeFileSync(join(savedDir, "dup.js"), "// saved copy");
102
+ const msg = deleteWorkflow("dup", () => false, { tmpDir, savedDir });
103
+ expect(msg).toBe(`Deleted workflow 'dup' (${join(tmpDir, "dup.js")})`);
104
+ expect(existsSync(join(tmpDir, "dup.js"))).toBe(false);
105
+ expect(existsSync(join(savedDir, "dup.js"))).toBe(true);
106
+ } finally {
107
+ rmSync(root, { recursive: true, force: true });
108
+ }
109
+ });
110
+
111
+ it("注入目录:tmp 无 → saved 候选删除", () => {
112
+ const { root, tmpDir, savedDir } = makeHostLayout();
113
+ try {
114
+ mkdirSync(savedDir, { recursive: true });
115
+ writeFileSync(join(savedDir, "gamma.js"), "// saved body");
116
+ const msg = deleteWorkflow("gamma", () => false, { tmpDir, savedDir });
117
+ expect(msg).toBe(`Deleted workflow 'gamma' (${join(savedDir, "gamma.js")})`);
118
+ expect(existsSync(join(savedDir, "gamma.js"))).toBe(false);
119
+ } finally {
120
+ rmSync(root, { recursive: true, force: true });
121
+ }
122
+ });
123
+
124
+ it("运行中 → 拒(文案逐字,防删运行中脚本)", () => {
125
+ const { root, tmpDir, savedDir } = makeHostLayout();
126
+ try {
127
+ seedTmp(tmpDir, "busy");
128
+ expect(() =>
129
+ deleteWorkflow("busy", (name) => name === "busy", { tmpDir, savedDir }),
130
+ ).toThrow("Cannot delete 'busy': workflow is currently running. Abort it first.");
131
+ expect(existsSync(join(tmpDir, "busy.js"))).toBe(true);
132
+ } finally {
133
+ rmSync(root, { recursive: true, force: true });
134
+ }
135
+ });
136
+
137
+ it("两候选均无 → 拒(文案逐字)", () => {
138
+ const { root, tmpDir, savedDir } = makeHostLayout();
139
+ try {
140
+ expect(() => deleteWorkflow("void", () => false, { tmpDir, savedDir })).toThrow(
141
+ "Workflow file 'void' not found",
142
+ );
143
+ } finally {
144
+ rmSync(root, { recursive: true, force: true });
145
+ }
146
+ });
147
+ });
148
+
149
+ // ============================================================
150
+ // 缺省目录(pi 布局回归——向后兼容:pi 现两参调用形态行为不变)
151
+ // ============================================================
152
+
153
+ describe("缺省目录 = pi 布局(向后兼容回归)", () => {
154
+ it("save 缺省:<cwd>/.pi/workflows/.tmp → <cwd>/.pi/workflows", async () => {
155
+ const fakeCwd = mkdtempSync(join(tmpdir(), "subagent-core-cwd-"));
156
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(fakeCwd);
157
+ try {
158
+ const tmpDir = join(fakeCwd, ".pi", "workflows", ".tmp");
159
+ mkdirSync(tmpDir, { recursive: true });
160
+ seedTmp(tmpDir, "pi-wf", "// pi layout");
161
+ const msg = await saveWorkflow("pi-wf");
162
+ expect(msg).toBe(`Saved 'pi-wf' → 'pi-wf' (${join(fakeCwd, ".pi", "workflows", "pi-wf.js")})`);
163
+ expect(existsSync(join(tmpDir, "pi-wf.js"))).toBe(false);
164
+ expect(existsSync(join(fakeCwd, ".pi", "workflows", "pi-wf.js"))).toBe(true);
165
+ } finally {
166
+ cwdSpy.mockRestore();
167
+ rmSync(fakeCwd, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ it("delete 缺省:两候选 = .pi/workflows/.tmp 与 .pi/workflows", () => {
172
+ const fakeCwd = mkdtempSync(join(tmpdir(), "subagent-core-cwd-"));
173
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(fakeCwd);
174
+ try {
175
+ const savedDir = join(fakeCwd, ".pi", "workflows");
176
+ mkdirSync(savedDir, { recursive: true });
177
+ writeFileSync(join(savedDir, "saved-only.js"), "// body");
178
+ const msg = deleteWorkflow("saved-only", () => false);
179
+ expect(msg).toBe(`Deleted workflow 'saved-only' (${join(savedDir, "saved-only.js")})`);
180
+ expect(existsSync(join(savedDir, "saved-only.js"))).toBe(false);
181
+ } finally {
182
+ cwdSpy.mockRestore();
183
+ rmSync(fakeCwd, { recursive: true, force: true });
184
+ }
185
+ });
186
+ });
@@ -0,0 +1,119 @@
1
+ // workflow-run-summary.test.ts —— runSummary 投影 + isScriptRunning 判定测试(U7/B5/D8)。
2
+ //
3
+ // 覆盖(验收条款③):
4
+ // - runSummary:字段投影全断言(running / done 两形态;slug/error/completedAt 缺省透传)
5
+ // - isScriptRunning:真(同名 running)/ 假(同名 done、异名 running、空 Map)两分支
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ import { isScriptRunning, runSummary } from "../workflow-run-summary.ts";
9
+ import { Budget } from "../models/budget.ts";
10
+ import type { RunSpec } from "../models/run-spec.ts";
11
+ import { Trace } from "../models/trace.ts";
12
+ import { WorkflowRun } from "../models/workflow-run.ts";
13
+
14
+ function makeSpec(scriptName: string, slug?: string): RunSpec {
15
+ return {
16
+ scriptSource: "execute() {}",
17
+ args: {},
18
+ scriptName,
19
+ ...(slug !== undefined ? { slug } : {}),
20
+ scriptPath: "/fake/test.js",
21
+ };
22
+ }
23
+
24
+ function makeRun(
25
+ runId: string,
26
+ opts: {
27
+ status?: "running" | "done";
28
+ scriptName?: string;
29
+ slug?: string;
30
+ reason?: "completed" | "failed" | "aborted" | "time_limited";
31
+ error?: string;
32
+ startedAt?: string;
33
+ completedAt?: string;
34
+ } = {},
35
+ ): WorkflowRun {
36
+ const status = opts.status ?? "running";
37
+ return WorkflowRun.reconstruct(
38
+ runId,
39
+ makeSpec(opts.scriptName ?? "deploy-site", opts.slug),
40
+ {
41
+ status,
42
+ ...(status === "done" ? { reason: opts.reason ?? "completed" } : {}),
43
+ budget: new Budget({ maxTokens: 1000 }),
44
+ calls: new Map(),
45
+ trace: new Trace(),
46
+ errorLogs: [],
47
+ ...(opts.error !== undefined ? { error: opts.error } : {}),
48
+ },
49
+ {
50
+ startedAt: opts.startedAt ?? "2026-08-30T00:00:00.000Z",
51
+ ...(opts.completedAt !== undefined ? { completedAt: opts.completedAt } : {}),
52
+ },
53
+ );
54
+ }
55
+
56
+ describe("runSummary — 字段投影(字段以 core WorkflowRun 为准)", () => {
57
+ it("running run:name=scriptName、slug、status、startedAt 投影;completedAt/reason 为 undefined", () => {
58
+ const run = makeRun("wf-r1", { status: "running", scriptName: "deploy-site", slug: "deploy" });
59
+
60
+ expect(runSummary(run)).toEqual({
61
+ runId: "wf-r1",
62
+ name: "deploy-site",
63
+ slug: "deploy",
64
+ status: "running",
65
+ reason: undefined,
66
+ startedAt: "2026-08-30T00:00:00.000Z",
67
+ completedAt: undefined,
68
+ error: undefined,
69
+ });
70
+ });
71
+
72
+ it("done run:reason/completedAt/error 投影(对齐 pi toRunSummary 字段集)", () => {
73
+ const run = makeRun("wf-d1", {
74
+ status: "done",
75
+ reason: "failed",
76
+ error: "agent timeout",
77
+ startedAt: "2026-08-30T10:00:00.000Z",
78
+ completedAt: "2026-08-30T10:05:00.000Z",
79
+ });
80
+
81
+ expect(runSummary(run)).toEqual({
82
+ runId: "wf-d1",
83
+ name: "deploy-site",
84
+ slug: undefined,
85
+ status: "done",
86
+ reason: "failed",
87
+ startedAt: "2026-08-30T10:00:00.000Z",
88
+ completedAt: "2026-08-30T10:05:00.000Z",
89
+ error: "agent timeout",
90
+ });
91
+ });
92
+ });
93
+
94
+ describe("isScriptRunning — 真假两分支", () => {
95
+ it("真:同名 script 仍在 running", () => {
96
+ const runs = new Map<string, WorkflowRun>([
97
+ ["wf-1", makeRun("wf-1", { status: "done", scriptName: "other-wf" })],
98
+ ["wf-2", makeRun("wf-2", { status: "running", scriptName: "deploy-site" })],
99
+ ]);
100
+
101
+ expect(isScriptRunning(runs, "deploy-site")).toBe(true);
102
+ });
103
+
104
+ it("假:同名但已 done(running 状态白名单)", () => {
105
+ const runs = new Map<string, WorkflowRun>([
106
+ ["wf-1", makeRun("wf-1", { status: "done", scriptName: "deploy-site" })],
107
+ ]);
108
+
109
+ expect(isScriptRunning(runs, "deploy-site")).toBe(false);
110
+ });
111
+
112
+ it("假:异名 running / 空 Map", () => {
113
+ const runs = new Map<string, WorkflowRun>([
114
+ ["wf-1", makeRun("wf-1", { status: "running", scriptName: "other-wf" })],
115
+ ]);
116
+ expect(isScriptRunning(runs, "deploy-site")).toBe(false);
117
+ expect(isScriptRunning(new Map(), "deploy-site")).toBe(false);
118
+ });
119
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * workflow-script-registry-impl 测试:sink 设计 U1 导出形态。
3
+ *
4
+ * 覆盖:WorkflowScript 类 re-export 可用性 + loadWorkflowScriptByPath 工厂加载
5
+ * (真实临时脚本文件)、非法引用拒绝(含 ⛔2 `..` 段收紧对工厂生效)、
6
+ * available=false stub 语义。
7
+ *
8
+ * 策略对齐 config-loader.test.ts:真实临时目录 + 真实脚本文件(meta 提取走真实
9
+ * 文件读取),只 mock findWorkspaceRoot(缓存桶 key,耦合 process.cwd 难隔离)与
10
+ * homedir(`~/` 引用展开锚点)。
11
+ */
12
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+
17
+ vi.mock("node:os", async (importOriginal) => {
18
+ const actual = await importOriginal<typeof import("node:os")>();
19
+ return { ...actual, homedir: vi.fn(() => "/nonexistent-home-for-tests") };
20
+ });
21
+
22
+ vi.mock("../../shared/resource-discovery.ts", async (importOriginal) => {
23
+ const actual = await importOriginal<
24
+ typeof import("../../shared/resource-discovery.ts")
25
+ >();
26
+ return {
27
+ ...actual,
28
+ findWorkspaceRoot: vi.fn(() => "/unused-by-default"),
29
+ };
30
+ });
31
+
32
+ import { invalidateCache } from "../config-loader.ts";
33
+ import { loadWorkflowScriptByPath, WorkflowScript } from "../workflow-script-registry-impl.ts";
34
+ import { homedir } from "node:os";
35
+
36
+ const mockedHomedir = vi.mocked(homedir);
37
+
38
+ /** 有效 workflow 脚本内容(@pi-meta 块注释 + agent 入口,同 config-loader.test 范式;phases 须 YAML 数组)。 */
39
+ const SCRIPT_SOURCE =
40
+ "/* @pi-meta\nname: factory-wf\ndescription: factory test\nphases: []\n*/\nagent({ prompt: \"x\" });\n";
41
+
42
+ async function writeScript(content: string, fileName = "factory-wf.js"): Promise<string> {
43
+ const dir = await mkdtemp(join(tmpdir(), "wf-registry-impl-test-"));
44
+ const p = join(dir, fileName);
45
+ await writeFile(p, content, "utf-8");
46
+ return p;
47
+ }
48
+
49
+ describe("loadWorkflowScriptByPath / WorkflowScript 导出形态(U1)", () => {
50
+ let tempDirs: string[] = [];
51
+
52
+ beforeEach(() => {
53
+ invalidateCache();
54
+ // 重置 `~/` 用例留下的 homedir mock 残留(mockReturnValue 跨测试持续)
55
+ mockedHomedir.mockReturnValue("/nonexistent-home-for-tests");
56
+ tempDirs = [];
57
+ });
58
+
59
+ afterEach(async () => {
60
+ invalidateCache();
61
+ for (const dir of tempDirs) {
62
+ await rm(dir, { recursive: true, force: true });
63
+ }
64
+ });
65
+
66
+ async function trackedWrite(content: string, fileName?: string): Promise<string> {
67
+ const p = await writeScript(content, fileName);
68
+ tempDirs.push(join(p, ".."));
69
+ return p;
70
+ }
71
+
72
+ it("按绝对路径加载:返回 WorkflowScript 实例(meta/sourceCode 填充,available=true)", async () => {
73
+ const p = await trackedWrite(SCRIPT_SOURCE);
74
+ const script = await loadWorkflowScriptByPath(p);
75
+ expect(script).toBeInstanceOf(WorkflowScript);
76
+ expect(script!.name).toBe("factory-wf");
77
+ expect(script!.available).toBe(true);
78
+ expect(script!.path).toBe(p);
79
+ expect(script!.sourceCode).toBe(SCRIPT_SOURCE);
80
+ expect(script!.meta.description).toBe("factory test");
81
+ });
82
+
83
+ it("re-export 的 WorkflowScript 类与实例同源:validate/toExecutable 实体操作可用", async () => {
84
+ const p = await trackedWrite(SCRIPT_SOURCE);
85
+ const script = await loadWorkflowScriptByPath(p);
86
+ expect(script).toBeInstanceOf(WorkflowScript);
87
+ // 实体操作(validate 委托 lintScript / toExecutable 返回可执行源)经导出类实例可用
88
+ const lint = script!.validate();
89
+ expect(lint.valid).toBe(true);
90
+ expect(Array.isArray(lint.findings)).toBe(true);
91
+ expect(script!.toExecutable()).toBe(SCRIPT_SOURCE);
92
+ });
93
+
94
+ it("文件不可读/不存在:available=false 的 stub(非 undefined——loader never throws)", async () => {
95
+ const p = await trackedWrite(SCRIPT_SOURCE, "gone.js");
96
+ await rm(p);
97
+ const script = await loadWorkflowScriptByPath(p);
98
+ expect(script).toBeDefined();
99
+ expect(script!.available).toBe(false);
100
+ });
101
+
102
+ it("非法引用返回 undefined:相对路径 / 非 .js 扩展名", async () => {
103
+ await trackedWrite(SCRIPT_SOURCE);
104
+ expect(await loadWorkflowScriptByPath("relative/wf.js")).toBeUndefined();
105
+ expect(await loadWorkflowScriptByPath("/proj/wf.txt")).toBeUndefined();
106
+ });
107
+
108
+ it("⛔2:含 .. 段的路径引用被拒(undefined——收紧对工厂消费面生效)", async () => {
109
+ await trackedWrite(SCRIPT_SOURCE);
110
+ expect(await loadWorkflowScriptByPath("/x/../factory-wf.js")).toBeUndefined();
111
+ });
112
+
113
+ it("~/ 前缀引用:展开 homedir 后加载(barrel 消费面的合法引用形态)", async () => {
114
+ const fakeHome = await mkdtemp(join(tmpdir(), "wf-registry-impl-home-"));
115
+ tempDirs.push(fakeHome);
116
+ mockedHomedir.mockReturnValue(fakeHome);
117
+ const p = join(fakeHome, "home-wf.js");
118
+ await writeFile(p, SCRIPT_SOURCE, "utf-8");
119
+ const script = await loadWorkflowScriptByPath("~/home-wf.js");
120
+ expect(script).toBeInstanceOf(WorkflowScript);
121
+ expect(script!.name).toBe("factory-wf");
122
+ expect(script!.path).toBe(p);
123
+ });
124
+ });
@@ -145,7 +145,7 @@ export function formatSchemaInstruction(schema: Record<string, unknown>): string
145
145
  export function resolveAgentOpts(opts: AgentCallOpts): ResolveResult {
146
146
  const appendSystemPrompt: string[] = [];
147
147
 
148
- // Resolve skill name to SKILL.md path
148
+ // Resolve skill name to SKILL.md path
149
149
  if (opts.skill) {
150
150
  const skillPath = resolveSkillPath(opts.skill);
151
151
  if (!skillPath) {
@@ -154,17 +154,17 @@ export function resolveAgentOpts(opts: AgentCallOpts): ResolveResult {
154
154
  opts = { ...opts, skillPath };
155
155
  }
156
156
 
157
- // Inject schema as structured-output instruction into appendSystemPrompt (content,
158
- // not temp file) and set environment variable for conditional tool + hook activation.
159
- // M2 fix: previously wrote the instruction to a temp file and pushed the FILE PATH,
160
- // which got concatenated into the final append file as path garbage — the SO instruction
161
- // never reached the subprocess. Now the instruction content is pushed directly.
157
+ // Inject schema as structured-output instruction into appendSystemPrompt (content,
158
+ // not temp file) and set environment variable for conditional tool + hook activation.
159
+ // M2 fix: previously wrote the instruction to a temp file and pushed the FILE PATH,
160
+ // which got concatenated into the final append file as path garbage — the SO instruction
161
+ // never reached the subprocess. Now the instruction content is pushed directly.
162
162
  if (opts.schema) {
163
163
  // IF7(#13):formatSchemaInstruction 与 schemaEnv 对同一 schema 对象引用共享
164
164
  // WeakMap 缓存条目(compact stringify 整个 dispatch 只发生一次)
165
165
  appendSystemPrompt.push(formatSchemaInstruction(opts.schema));
166
166
 
167
- // Set env var for structured-output extension to activate tool + hook
167
+ // Set env var for structured-output extension to activate tool + hook
168
168
  opts = { ...opts, schemaEnv: stringifySchemaCached(opts.schema, "compact") };
169
169
  }
170
170