@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +15 -0
  2. package/package.json +7 -4
  3. package/scripts/rfl.mjs +308 -0
  4. package/skills/workflow-script-format/SKILL.md +1 -1
  5. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  6. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  7. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  8. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  9. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  10. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  11. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  12. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  13. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  14. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  15. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  16. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  17. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  18. package/src/execution/__tests__/execution-record.test.ts +199 -0
  19. package/src/execution/__tests__/finalize-record.test.ts +197 -15
  20. package/src/execution/__tests__/format.test.ts +131 -7
  21. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  22. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  23. package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
  24. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  25. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  26. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  27. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  28. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  29. package/src/execution/__tests__/list-component.test.ts +59 -5
  30. package/src/execution/__tests__/list-fields.test.ts +109 -0
  31. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  32. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  33. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  34. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  35. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  36. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  37. package/src/execution/__tests__/record-store.test.ts +442 -54
  38. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  39. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  40. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  41. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  42. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  43. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  44. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  45. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  46. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  47. package/src/execution/__tests__/session-pending.test.ts +61 -2
  48. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  49. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  50. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  51. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  52. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  53. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  54. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  55. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  56. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  57. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  59. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  60. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  61. package/src/execution/__tests__/tool-action.test.ts +12 -10
  62. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  63. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  64. package/src/execution/__tests__/worktree-manager.test.ts +300 -90
  65. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
  66. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  67. package/src/execution/__tests__/worktree-registry.test.ts +72 -34
  68. package/src/execution/agent-result-mapper.ts +4 -1
  69. package/src/execution/argv-mirror.ts +21 -2
  70. package/src/execution/channel-registry-access.ts +3 -1
  71. package/src/execution/execution-record.ts +126 -9
  72. package/src/execution/finalize-record.ts +99 -13
  73. package/src/execution/idle-gc.ts +47 -0
  74. package/src/execution/lifecycle-manager.ts +491 -0
  75. package/src/execution/lifecycle-predicates.ts +65 -0
  76. package/src/execution/manifest-store.ts +61 -16
  77. package/src/execution/model-resolver.ts +26 -5
  78. package/src/execution/notifier.ts +69 -12
  79. package/src/execution/pi-invocation.ts +21 -1
  80. package/src/execution/record-entry.ts +118 -0
  81. package/src/execution/record-store.ts +844 -108
  82. package/src/execution/session-pending.ts +121 -49
  83. package/src/execution/session-reconstructor.ts +224 -7
  84. package/src/execution/session-runner.ts +713 -316
  85. package/src/execution/sessions-index.ts +304 -0
  86. package/src/execution/stdin-writer.ts +93 -7
  87. package/src/execution/stream-sink.ts +20 -3
  88. package/src/execution/subagent-service.ts +917 -138
  89. package/src/execution/temp-prompt.ts +8 -3
  90. package/src/execution/turn-limiter.ts +14 -0
  91. package/src/execution/types.ts +218 -19
  92. package/src/execution/worktree-manager.ts +449 -59
  93. package/src/execution/worktree-registry.ts +97 -29
  94. package/src/index.ts +318 -20
  95. package/src/injectors/subagent-list-injector.ts +26 -8
  96. package/src/injectors/workflow-list-injector.ts +25 -8
  97. package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
  98. package/src/interface/__tests__/tool-render.test.ts +10 -8
  99. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
  100. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
  101. package/src/interface/bg-notify-render.ts +32 -8
  102. package/src/interface/command-actions.ts +26 -7
  103. package/src/interface/commands.ts +21 -22
  104. package/src/interface/format.ts +53 -20
  105. package/src/interface/gui-mappers.ts +6 -8
  106. package/src/interface/helpers.ts +170 -10
  107. package/src/interface/list-component.ts +53 -14
  108. package/src/interface/subagent-actions.ts +235 -17
  109. package/src/interface/subagent-tool.ts +81 -16
  110. package/src/interface/subagents.ts +2 -1
  111. package/src/interface/tool-render.ts +21 -27
  112. package/src/interface/tool-workflow-script.ts +29 -33
  113. package/src/interface/tool-workflow.ts +67 -100
  114. package/src/interface/views/WorkflowsView.ts +89 -32
  115. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  116. package/src/interface/views/detail-content.ts +1 -1
  117. package/src/interface/views/format.ts +3 -3
  118. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
  119. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  120. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  121. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  122. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  123. package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
  124. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  128. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  129. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
  130. package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
  131. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  132. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  133. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
  134. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  135. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  136. package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
  137. package/src/orchestration/agent-opts-resolver.ts +4 -1
  138. package/src/orchestration/args-validator.ts +2 -2
  139. package/src/orchestration/config-loader.ts +30 -1
  140. package/src/orchestration/error-recovery.ts +137 -30
  141. package/src/orchestration/execute-agent-call.ts +44 -10
  142. package/src/orchestration/jsonl-run-store.ts +397 -71
  143. package/src/orchestration/launcher.ts +7 -1
  144. package/src/orchestration/lifecycle.ts +145 -133
  145. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  146. package/src/orchestration/models/budget.ts +1 -1
  147. package/src/orchestration/models/run-runtime.ts +15 -17
  148. package/src/orchestration/models/run-spec.ts +2 -2
  149. package/src/orchestration/models/run-state.ts +3 -3
  150. package/src/orchestration/models/trace.ts +95 -15
  151. package/src/orchestration/models/types.ts +8 -9
  152. package/src/orchestration/models/workflow-run.ts +50 -71
  153. package/src/orchestration/models/workflow-script.ts +32 -1
  154. package/src/orchestration/skill-discovery.ts +30 -0
  155. package/src/orchestration/worker-handle.ts +1 -1
  156. package/src/orchestration/worker-host.ts +1 -1
  157. package/src/orchestration/worker-script-builder.ts +38 -11
  158. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  159. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  160. package/src/shared/__tests__/resource-discovery.test.ts +79 -0
  161. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  162. package/src/shared/agent-ref.ts +22 -1
  163. package/src/shared/resource-discovery.ts +162 -59
  164. package/src/shared/schema-jsonify.ts +56 -0
  165. package/workflows/review-fix-loop-utils.cjs +542 -32
  166. package/workflows/review-fix-loop.js +462 -109
@@ -0,0 +1,264 @@
1
+ /**
2
+ * computeRenderSignature(IF11/TC7/DM6)— 渲染签名单测(now 参数化)。
3
+ *
4
+ * 契约:tick 条件失效的判据——签名字段集覆盖 header(renderHeader)/节点行
5
+ * (renderLevel1 agent list)/L2 detail(buildDetailContent)当前消费的全部动态
6
+ * 字段。本测试证明「已入字段变化 → 签名变」+「静态 run 不变」(完备性无法靠
7
+ * 测试证明,字段核对表见 WorkflowsView.ts computeRenderSignature doc)。
8
+ *
9
+ * 确定性说明:签名非完全纯——live 节点 elapsedSeconds 由 projectLiveProgress 内
10
+ * computeElapsedSeconds 现算(record.endedAt ?? Date.now())。fake timers 控制
11
+ * Date.now() 与 now 参数同源推进,保证确定性。
12
+ */
13
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
14
+
15
+ import { computeRenderSignature } from "../WorkflowsView.ts";
16
+ import type { ExecutionRecord } from "../../../execution/types.ts";
17
+ import type { ExecutionTraceNode, WorkerLogEntry } from "../../../orchestration/models/types.ts";
18
+ import type { WorkflowRun } from "../../../orchestration/models/workflow-run.ts";
19
+
20
+ // ── Fixtures(duck typing,对齐 detail-content-session-file.test.ts 先例)──
21
+
22
+ const T0 = 1_700_000_000_000; // 固定 epoch ms
23
+
24
+ function makeLive(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
25
+ return {
26
+ id: "run-0",
27
+ agent: "worker",
28
+ model: "default",
29
+ thinkingLevel: undefined,
30
+ mode: "sync",
31
+ task: "t",
32
+ slug: "s",
33
+ startedAt: T0,
34
+ rootSessionId: undefined,
35
+ parentRecordId: undefined,
36
+ depth: 0,
37
+ status: "running",
38
+ turns: [],
39
+ turnCount: 0,
40
+ totalTokens: 0,
41
+ lastError: undefined,
42
+ endedAt: T0 + 5000,
43
+ result: undefined,
44
+ error: undefined,
45
+ agentResult: undefined,
46
+ ...overrides,
47
+ } as unknown as ExecutionRecord;
48
+ }
49
+
50
+ function makeNode(overrides: Partial<ExecutionTraceNode> = {}): ExecutionTraceNode {
51
+ return {
52
+ stepIndex: 0,
53
+ agent: "worker",
54
+ task: "do",
55
+ model: "default",
56
+ status: "running",
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ interface RunShape {
62
+ status?: string;
63
+ budget?: { usedTokens: number; maxTokens?: number; usedCost: number };
64
+ nodes?: ExecutionTraceNode[];
65
+ errorLogs?: WorkerLogEntry[];
66
+ }
67
+
68
+ function makeRun(shape: RunShape = {}): WorkflowRun {
69
+ return {
70
+ state: {
71
+ status: shape.status ?? "running",
72
+ budget: shape.budget ?? { usedTokens: 0, maxTokens: 200_000, usedCost: 0 },
73
+ trace: { toArray: () => shape.nodes ?? [] },
74
+ errorLogs: shape.errorLogs ?? [],
75
+ },
76
+ } as unknown as WorkflowRun;
77
+ }
78
+
79
+ beforeEach(() => {
80
+ vi.useFakeTimers();
81
+ vi.setSystemTime(T0);
82
+ });
83
+
84
+ afterEach(() => {
85
+ vi.useRealTimers();
86
+ });
87
+
88
+ describe("computeRenderSignature — 基础确定性", () => {
89
+ it("同输入同签名(含 live 节点,endedAt 固定使 elapsed 确定)", () => {
90
+ const run = makeRun({ nodes: [makeNode({ live: makeLive() })] });
91
+ expect(computeRenderSignature(run, T0)).toBe(computeRenderSignature(run, T0));
92
+ });
93
+
94
+ it("静态 run(无 live、同秒桶)200ms 内签名不变", () => {
95
+ const run = makeRun({ nodes: [makeNode({ status: "completed" })] });
96
+ expect(computeRenderSignature(run, T0)).toBe(computeRenderSignature(run, T0 + 200));
97
+ });
98
+
99
+ it("秒桶跨秒(now 相差 1s 桶)→ 签名变", () => {
100
+ const run = makeRun();
101
+ expect(computeRenderSignature(run, T0)).not.toBe(computeRenderSignature(run, T0 + 1000));
102
+ });
103
+ });
104
+
105
+ describe("computeRenderSignature — run 级字段", () => {
106
+ it("run.state.status 变化 → 签名变", () => {
107
+ const a = makeRun({ status: "running" });
108
+ const b = makeRun({ status: "done" });
109
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
110
+ });
111
+
112
+ it("completed/total 变化(节点 status 推导)→ 签名变", () => {
113
+ const a = makeRun({ nodes: [makeNode({ status: "running" })] });
114
+ const b = makeRun({ nodes: [makeNode({ status: "completed" })] });
115
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
116
+ });
117
+
118
+ it("budget tokens 量化值变化 → 签名变", () => {
119
+ const a = makeRun({ budget: { usedTokens: 1500, maxTokens: 200_000, usedCost: 0.01 } });
120
+ const b = makeRun({ budget: { usedTokens: 2500, maxTokens: 200_000, usedCost: 0.01 } });
121
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
122
+ });
123
+
124
+ it("budget cost 第 4 位小数变化(toFixed(4) 可见精度)→ 签名变", () => {
125
+ // 0.0100 vs 0.0101:量化展示相同到第 3 位,第 4 位是渲染可见精度
126
+ const a = makeRun({ budget: { usedTokens: 0, maxTokens: 200_000, usedCost: 0.0100 } });
127
+ const b = makeRun({ budget: { usedTokens: 0, maxTokens: 200_000, usedCost: 0.0101 } });
128
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
129
+ });
130
+
131
+ it("run.state.errorLogs 追加 → 签名变(末条内容入指纹)", () => {
132
+ const a = makeRun({ errorLogs: [{ level: "error", message: "E-0" }] });
133
+ const b = makeRun({ errorLogs: [{ level: "error", message: "E-0" }, { level: "warn", message: "W-1" }] });
134
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
135
+ });
136
+
137
+ it("errorLogs 封顶后 length 不变内容移(push+slice(-MAX_ERROR_LOGS))→ 签名仍变(指纹非 length)", () => {
138
+ // 模拟 error-recovery 的变异路径:push 后 slice(-500) 截断。两次 state 均 500 条
139
+ // (length 相同),仅末条/窗口内容不同——指纹含末条内容才不漏失效。
140
+ const MAX_ERROR_LOGS = 500;
141
+ const mk = (n: number): WorkerLogEntry[] => ({ level: "error", message: `E-${n}` });
142
+ const before: WorkerLogEntry[] = Array.from({ length: MAX_ERROR_LOGS }, (_, i) => mk(i));
143
+ const after = [...before, mk(MAX_ERROR_LOGS)].slice(-MAX_ERROR_LOGS); // E-1..E-500
144
+ expect(after).toHaveLength(MAX_ERROR_LOGS); // 前置校验:封顶后 length 不变
145
+ const a = makeRun({ errorLogs: before });
146
+ const b = makeRun({ errorLogs: after });
147
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
148
+ });
149
+ });
150
+
151
+ describe("computeRenderSignature — 节点级字段(live 投影)", () => {
152
+ it("节点 status 变化 → 签名变", () => {
153
+ const a = makeRun({ nodes: [makeNode({ status: "running" })] });
154
+ const b = makeRun({ nodes: [makeNode({ status: "failed" })] });
155
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
156
+ });
157
+
158
+ it("live.totalTokens 变化 → 签名变", () => {
159
+ const a = makeRun({ nodes: [makeNode({ live: makeLive({ totalTokens: 1000 }) })] });
160
+ const b = makeRun({ nodes: [makeNode({ live: makeLive({ totalTokens: 2000 }) })] });
161
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
162
+ });
163
+
164
+ it("getAllToolCalls(node.live).length 变化 → 签名变(toolCalls 追加)", () => {
165
+ const live1 = makeLive({
166
+ turns: [{ text: "", closed: false, toolCalls: [{ toolName: "read", args: {}, result: "", isError: false, _status: "completed", startedTs: T0 }] }],
167
+ });
168
+ const live2 = makeLive({
169
+ turns: [{ text: "", closed: false, toolCalls: [
170
+ { toolName: "read", args: {}, result: "", isError: false, _status: "completed", startedTs: T0 },
171
+ { toolName: "bash", args: {}, result: "", isError: false, _status: "completed", startedTs: T0 },
172
+ ] }],
173
+ });
174
+ const a = makeRun({ nodes: [makeNode({ live: live1 })] });
175
+ const b = makeRun({ nodes: [makeNode({ live: live2 })] });
176
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
177
+ });
178
+
179
+ it("live.elapsedSeconds 变化 → 签名变(running 节点跨秒)", () => {
180
+ // endedAt undefined → computeElapsedSeconds 用真实 Date.now();fake timers 推进 1s
181
+ const live = makeLive({ endedAt: undefined });
182
+ const run = makeRun({ nodes: [makeNode({ live })] });
183
+ const before = computeRenderSignature(run, T0);
184
+ vi.setSystemTime(T0 + 1000);
185
+ const after = computeRenderSignature(run, T0 + 1000);
186
+ expect(after).not.toBe(before);
187
+ });
188
+
189
+ it("live.turns 计数变化 → 签名变", () => {
190
+ const a = makeRun({ nodes: [makeNode({ live: makeLive({ turnCount: 1 }) })] });
191
+ const b = makeRun({ nodes: [makeNode({ live: makeLive({ turnCount: 2 }) })] });
192
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
193
+ });
194
+
195
+ it("live.eventLog 追加(turn 闭合产生 turn_end 条目)→ 签名变", () => {
196
+ const live1 = makeLive({
197
+ turns: [{ text: "partial", closed: false, toolCalls: [] }],
198
+ });
199
+ const live2 = makeLive({
200
+ turns: [{ text: "partial", closed: true, closedTs: T0, toolCalls: [] }],
201
+ });
202
+ const a = makeRun({ nodes: [makeNode({ live: live1 })] });
203
+ const b = makeRun({ nodes: [makeNode({ live: live2 })] });
204
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
205
+ });
206
+
207
+ it("live.currentActivity 出现 / 变化(type+label)→ 签名变", () => {
208
+ const noActivity = makeLive({ turns: [{ text: "", closed: false, toolCalls: [] }] });
209
+ const toolRunning = makeLive({
210
+ turns: [{ text: "", closed: false, toolCalls: [{ toolName: "write", args: {}, result: "", isError: false, _status: "running", startedTs: T0 }] }],
211
+ });
212
+ const toolRunningOtherLabel = makeLive({
213
+ turns: [{ text: "", closed: false, toolCalls: [{ toolName: "bash", args: {}, result: "", isError: false, _status: "running", startedTs: T0 }] }],
214
+ });
215
+ const base = makeRun({ nodes: [makeNode({ live: noActivity })] });
216
+ const withTool = makeRun({ nodes: [makeNode({ live: toolRunning })] });
217
+ const otherLabel = makeRun({ nodes: [makeNode({ live: toolRunningOtherLabel })] });
218
+
219
+ const s0 = computeRenderSignature(base, T0);
220
+ expect(computeRenderSignature(withTool, T0)).not.toBe(s0); // 出现
221
+ expect(computeRenderSignature(otherLabel, T0)).not.toBe(computeRenderSignature(withTool, T0)); // label 变
222
+ });
223
+
224
+ it("live.lastError 出现(内容入签名)→ 签名变", () => {
225
+ const a = makeRun({ nodes: [makeNode({ live: makeLive({ lastError: undefined }) })] });
226
+ const b = makeRun({ nodes: [makeNode({ live: makeLive({ lastError: "EPIPE: broken pipe" }) })] });
227
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
228
+ });
229
+
230
+ it("node.sessionFile 出现 → 签名变", () => {
231
+ const a = makeRun({ nodes: [makeNode({})] });
232
+ const b = makeRun({ nodes: [makeNode({ sessionFile: "/tmp/sessions/run-0.jsonl" })] });
233
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
234
+ });
235
+ });
236
+
237
+ describe("computeRenderSignature — 多节点与无 live 终态", () => {
238
+ it("多节点逐一拼接(节点序参与签名)", () => {
239
+ const run = makeRun({
240
+ nodes: [
241
+ makeNode({ stepIndex: 0, status: "completed" }),
242
+ makeNode({ stepIndex: 1, status: "running", live: makeLive({ totalTokens: 500 }) }),
243
+ ],
244
+ });
245
+ const sig = computeRenderSignature(run, T0);
246
+ expect(sig).toContain("0:completed:-:-1:-1:-1:-1:-1:-:-");
247
+ expect(sig).toContain("1:running:-:500:0:5:0:0:-:-");
248
+ });
249
+
250
+ it("节点重排(trace 数组序对调,stepIndex 维度)→ 签名变", () => {
251
+ // 同一节点集、字段值均不变,仅 trace 数组顺序对调——nodeParts 按 trace 序拼接,
252
+ // 首字段 stepIndex 随之换位,签名必变(不漏失效)。
253
+ const first = makeNode({ stepIndex: 0, status: "completed" });
254
+ const second = makeNode({ stepIndex: 1, status: "running", live: makeLive() });
255
+ const a = makeRun({ nodes: [first, second] });
256
+ const b = makeRun({ nodes: [second, first] });
257
+ expect(computeRenderSignature(a, T0)).not.toBe(computeRenderSignature(b, T0));
258
+ });
259
+
260
+ it("无节点 run 签名仅含 run 级五段(status/秒桶/completed-total/budget/errorLogs)", () => {
261
+ const sig = computeRenderSignature(makeRun({ nodes: [] }), T0);
262
+ expect(sig.split("|")).toHaveLength(5);
263
+ });
264
+ });
@@ -272,7 +272,7 @@ function renderOutcomeSection(
272
272
  if (node.status === "running" && node.live) {
273
273
  // 运行中:显示实时指标(elapsed/tokens/turns)替代空荡的 "Still running..."
274
274
  const live = projectLiveProgress(node.live);
275
- const tokK = live.totalTokens > 0 ? `${Math.round(live.totalTokens / 1000)}k tok` : "0 tok";
275
+ const tokK = live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "0 tok";
276
276
  rightLines.push(theme.fg("dim", ` Running · ${formatElapsedSeconds(live.elapsedSeconds)} · ${tokK} · ${live.turns} turn${live.turns !== 1 ? "s" : ""}`));
277
277
  if (live.lastError) {
278
278
  rightLines.push(theme.fg("warning", ` ⚠ ${live.lastError.slice(0, mainWidth - BOX_BORDER_CHARS)}`));
@@ -10,6 +10,7 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import type { AgentEventLogEntry } from "../../execution/types.ts";
11
11
  import type { ExecutionTraceNode, ToolCallEntry } from "../../orchestration/models/types.ts";
12
12
  import type { DoneReason, RunStatus } from "../../orchestration/models/types.ts";
13
+ import { displayAgentName } from "../../shared/agent-ref.ts";
13
14
 
14
15
  // ── Constants ─────────────────────────────────────────────────
15
16
 
@@ -39,7 +40,7 @@ const SECS_PER_MIN = 60;
39
40
  /**
40
41
  * 可显示的状态文本集合。
41
42
  *
42
- * 包含 RunStatus("running"|"paused"|"done" 不直接显示,转 reason)+ DoneReason
43
+ * 包含 RunStatus("running"|"done" 不直接显示,转 reason)+ DoneReason
43
44
  * (completed/failed/aborted/budget_limited/time_limited)+ ExecutionTraceNode.status
44
45
  * (含 "pending"——trace 节点的初始态)。
45
46
  *
@@ -85,7 +86,6 @@ export function formatStatusBadge(
85
86
  ): string {
86
87
  switch (status) {
87
88
  case "running": return theme.fg("warning", "\u25CF running");
88
- case "paused": return theme.fg("warning", "\u23F8 PAUSED");
89
89
  case "completed": return theme.fg("success", "\u2713 completed");
90
90
  case "failed": return theme.fg("error", "\u2717 failed");
91
91
  case "aborted": return theme.fg("error", "\u2717 aborted");
@@ -311,7 +311,7 @@ export function formatAgentOneLiner(node: ExecutionTraceNode, theme: ThemeLike):
311
311
  ? `${Math.round((tok.input + tok.output) / TOKEN_K)}k tok`
312
312
  : "";
313
313
  const tcCount = node.result?.toolCalls?.length ?? 0;
314
- const parts = [dot, node.agent, node.model];
314
+ const parts = [dot, displayAgentName(node.agent), node.model];
315
315
  if (tokStr) parts.push(`${tokStr} · ${tcCount} tools`);
316
316
  parts.push(elapsed);
317
317
  return parts.join(" ");
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ // Module-scope helpers: accessible to BOTH the IIFE body AND the outer
3
+ // .then()/.catch() handlers (which run outside the IIFE). _workerLogs/
4
+ // _pushWorkerLog/_safePost + the parentPort/workerData handles must all live
5
+ // here — a previous version declared _safePost inside the IIFE, so the
6
+ // .then()/.catch() return/error handlers threw ReferenceError: _safePost is
7
+ // not defined, crashing the Worker on EVERY script return (exit code 1) and
8
+ // losing all diagnostics. require() is cached, so destructuring once at module
9
+ // load and reusing everywhere avoids the redundant require calls that used to
10
+ // appear in the IIFE and the outer .then/.catch.
11
+ const { parentPort: _parentPort, workerData: _workerData } = require("node:worker_threads");
12
+ const _workerLogs = [];
13
+ // IF6(#12): known agent() fields — hoisted to module scope, built once per worker
14
+ // (was rebuilt inside agent() on every call; field set is call-invariant).
15
+ const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);
16
+ function _pushWorkerLog(level, args) {
17
+ try { _workerLogs.push({ level, message: args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") }); } catch (e) { /* swallow */ }
18
+ }
19
+ // ── safePostMessage wrapper: 统一 postMessage 防御(DataCloneError 等)──
20
+ // Module-scope so the outer .then/.catch return/error handlers can use it.
21
+ // context 取值约定(诊断标识):固定为消息类型字面量——
22
+ // "agent-call" / "workflow-call" / "return" / "error",调用方据此在日志里
23
+ // 一眼定位是哪类 postMessage 失败。新增调用点必须传对应 context。
24
+ function _safePost(msg, context) {
25
+ try { _parentPort.postMessage(msg); return true; }
26
+ catch (e) {
27
+ const errMsg = e && e.message ? e.message : String(e);
28
+ const stack = e && e.stack ? e.stack : "";
29
+ _pushWorkerLog("error", ["[postMessage failed:" + context + "]", errMsg, stack]);
30
+ return false;
31
+ }
32
+ }
33
+ (async () => {
34
+ const parentPort = _parentPort;
35
+ const workerData = _workerData;
36
+
37
+ if (!parentPort) {
38
+ throw new Error("Workflow worker: parentPort is null — not running in a Worker thread");
39
+ }
40
+
41
+ // ── Intercept console.* to avoid leaking worker diagnostics into the input area ──
42
+ console.log = function (...args) { _pushWorkerLog("log", args); };
43
+ console.warn = function (...args) { _pushWorkerLog("warn", args); };
44
+ console.error = function (...args) { _pushWorkerLog("error", args); };
45
+ console.info = function (...args) { _pushWorkerLog("info", args); };
46
+
47
+ // ── Internal state ──
48
+ let _callIdCounter = 0;
49
+ let _agentCallCount = 0;
50
+ const _pendingCalls = new Map();
51
+ const _callCache = workerData.callCache instanceof Map
52
+ ? workerData.callCache
53
+ : new Map(Object.entries(workerData.callCache || {}).map(([k, v]) => [Number(k), v]));
54
+
55
+ // ── Injected globals ──
56
+ const $ARGS = (workerData.args && typeof workerData.args === "object") ? workerData.args : {};
57
+ const args = $ARGS;
58
+ const $WORKSPACE = typeof workerData.workspace === "string" ? workerData.workspace : "";
59
+ const _budgetData = {
60
+ total: (workerData.budget && workerData.budget.maxTokens) || 0,
61
+ _spentTokens: (workerData.budget && workerData.budget.usedTokens) ?? 0,
62
+ _spentCost: (workerData.budget && workerData.budget.usedCost) ?? 0,
63
+ };
64
+ const $BUDGET = {
65
+ get total() { return _budgetData.total; },
66
+ spent() { return _budgetData._spentTokens; },
67
+ remaining() { return Math.max(0, _budgetData.total - _budgetData._spentTokens); },
68
+ };
69
+
70
+ // ── Run-level model/thinkingLevel override (Option B symmetric single-path) ──
71
+ // Injected from workerData (set by workerHost from RunSpec.model/thinkingLevel).
72
+ // agent() reads these as run-level defaults; per-call explicit opts still win.
73
+ const $MODEL = (workerData.model && typeof workerData.model === "string") ? workerData.model : undefined;
74
+ const $THINKING_LEVEL = (workerData.thinkingLevel && typeof workerData.thinkingLevel === "string") ? workerData.thinkingLevel : undefined;
75
+
76
+ // ── WorkflowAbortedError ──
77
+ class WorkflowAbortedError extends Error {
78
+ constructor(reason) {
79
+ super("Workflow aborted: " + (reason || "No reason"));
80
+ this.name = "WorkflowAbortedError";
81
+ this.reason = reason || "";
82
+ }
83
+ }
84
+
85
+ // ── Message handler (main thread → worker) ──
86
+ parentPort.on("message", (msg) => {
87
+ if (msg.type === "agent-result") {
88
+ const pending = _pendingCalls.get(msg.callId);
89
+ if (pending) {
90
+ _pendingCalls.delete(msg.callId);
91
+ if (typeof msg.result !== "undefined") {
92
+ _callCache.set(msg.callId, msg.result);
93
+ }
94
+ // 失败不传播到 agent 外部:resolve 而非 reject。
95
+ // 旧实现在 result.error 时 reject,单 agent 失败会冒到 worker 顶层 .catch()
96
+ // → 发 type:"error" → handleScriptError → rebuildRuntime → SIGKILL 同伴进程,
97
+ // 把单点失败放大成整批崩溃。改为始终 resolve(错误时回退到 content 文本),
98
+ // 让 parallel() 下的脚本容错循环(parseResult → null → skip)自然接管。
99
+ // 错误原因已由主线程 executeAgentCall → trace.update(result.error) 保留在 trace/TUI,
100
+ // 不丢失。失败 resolve 为空字符串是既定容错策略。
101
+ // parsedOutput: validated data object from structured-output execute().
102
+ // Fallback to content (raw text) when no schema was requested or on error.
103
+ // W2 改动 9(b):returnMeta===true 时 resolve {value,sessionFile,worktreePath,error,usage,durationMs,sessionId}
104
+ // (对称缓存重放 9c);否则单值。usage/durationMs/sessionId 为 rfl 仪表透传(tier-1 §7.1,
105
+ // AgentResult 已含、此处原样转发,缺省 undefined 兼容旧主线程)。
106
+ const _value = msg.result.parsedOutput ?? msg.result.content;
107
+ if (pending.returnMeta) {
108
+ pending.resolve({
109
+ value: _value,
110
+ sessionFile: msg.result.sessionFile,
111
+ worktreePath: msg.result.worktreePath,
112
+ error: msg.result.error,
113
+ usage: msg.result.usage,
114
+ durationMs: msg.result.durationMs,
115
+ sessionId: msg.result.sessionId,
116
+ });
117
+ } else {
118
+ pending.resolve(_value);
119
+ }
120
+ }
121
+ } else if (msg.type === "workflow-result") {
122
+ const pending = _pendingCalls.get(msg.callId);
123
+ if (pending) {
124
+ _pendingCalls.delete(msg.callId);
125
+ pending.resolve(msg.result);
126
+ }
127
+ } else if (msg.type === "abort") {
128
+ const err = new WorkflowAbortedError(msg.reason);
129
+ _pendingCalls.forEach((p) => { p.reject(err); });
130
+ _pendingCalls.clear();
131
+ } else if (msg.type === "budget-update" && msg.budget) {
132
+ _budgetData._spentTokens = msg.budget.usedTokens ?? _budgetData._spentTokens;
133
+ _budgetData._spentCost = msg.budget.usedCost ?? _budgetData._spentCost;
134
+ }
135
+ // "budget-warning" is informational; no required handling
136
+ });
137
+
138
+ let _currentPhase = "";
139
+ function phase(name) { _currentPhase = String(name); }
140
+
141
+ function log(msg) {
142
+ try { parentPort.postMessage({ type: "log", phase: _currentPhase, message: String(msg) }); } catch(e) { /* swallow */ }
143
+ }
144
+
145
+ async function agent(firstArg, secondArg) {
146
+ let opts;
147
+ if (typeof firstArg === "string") {
148
+ opts = {
149
+ prompt: firstArg,
150
+ description: (secondArg && typeof secondArg === "object" && secondArg.label) || undefined,
151
+ schema: (secondArg && typeof secondArg === "object" && secondArg.schema) || undefined,
152
+ model: (secondArg && typeof secondArg === "object" && secondArg.model) || $MODEL,
153
+ scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,
154
+ phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,
155
+ thinkingLevel: (secondArg && typeof secondArg === "object" && secondArg.thinkingLevel) || $THINKING_LEVEL,
156
+ };
157
+ } else if (typeof firstArg === "object" && firstArg !== null) {
158
+ if (firstArg.prompt) {
159
+ opts = firstArg;
160
+ } else if (firstArg.task || firstArg.agent) {
161
+ // fork/worktree/returnMeta forwarded here (parity with agent({prompt,...}) and direct-pass
162
+ // branches). Previously this shortcut dropped them (w1 only noted, not wired).
163
+ opts = {
164
+ prompt: firstArg.task || firstArg.prompt || "",
165
+ description: firstArg.label || firstArg.description,
166
+ agent: firstArg.agent,
167
+ schema: firstArg.schema,
168
+ model: firstArg.model || $MODEL,
169
+ scene: firstArg.scene,
170
+ skill: firstArg.skill,
171
+ timeoutMs: firstArg.timeoutMs,
172
+ cwd: firstArg.cwd,
173
+ fork: firstArg.fork,
174
+ worktree: firstArg.worktree,
175
+ returnMeta: firstArg.returnMeta,
176
+ thinkingLevel: firstArg.thinkingLevel || $THINKING_LEVEL,
177
+ };
178
+ } else {
179
+ opts = firstArg;
180
+ }
181
+ } else {
182
+ throw new Error("agent() requires a prompt string or options object as first argument");
183
+ }
184
+
185
+ // Run-level fallback for object branches (opts = firstArg direct assign).
186
+ // Priority: agent() explicit > $MODEL global (set from workerData.model).
187
+ if (!opts.model && $MODEL) opts.model = $MODEL;
188
+ if (!opts.thinkingLevel && $THINKING_LEVEL) opts.thinkingLevel = $THINKING_LEVEL;
189
+
190
+ // Validate known agent() fields to catch API misuse early (_KNOWN_FIELDS at module scope)
191
+ const _unknownFields = Object.keys(opts).filter((k) => !_KNOWN_FIELDS.has(k));
192
+ if (_unknownFields.length > 0) {
193
+ _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel"]);
194
+ }
195
+
196
+ const callId = _callIdCounter;
197
+ _callIdCounter++;
198
+ _agentCallCount++;
199
+ if (_callCache.has(callId)) {
200
+ const cached = _callCache.get(callId);
201
+ // 与 live handler 对齐:失败也 resolve(回退 content),不 throw。
202
+ // 见 agent-result 消息处理的注释:拒绝传播失败到 agent 外部。
203
+ // W2 改动 9(c):returnMeta 分支(对称 handler 9b),opts 仍在闭包作用域。
204
+ if (!cached) return undefined;
205
+ const _cachedValue = cached.parsedOutput ?? cached.content;
206
+ if (opts.returnMeta === true) {
207
+ return {
208
+ value: _cachedValue,
209
+ sessionFile: cached.sessionFile,
210
+ worktreePath: cached.worktreePath,
211
+ error: cached.error,
212
+ usage: cached.usage,
213
+ durationMs: cached.durationMs,
214
+ sessionId: cached.sessionId,
215
+ };
216
+ }
217
+ return _cachedValue;
218
+ }
219
+
220
+ const _effectivePhase = opts.phase || _currentPhase;
221
+ delete opts.phase;
222
+
223
+ if (!_safePost({ type: "agent-call", callId, opts, phase: _effectivePhase }, "agent-call")) {
224
+ return Promise.reject(new Error("postMessage failed for agent-call (callId=" + callId + "): see workerLogs"));
225
+ }
226
+ return new Promise((resolve, reject) => {
227
+ _pendingCalls.set(callId, { resolve, reject, returnMeta: opts.returnMeta === true });
228
+ });
229
+ }
230
+
231
+ async function parallel(calls) {
232
+ if (typeof calls === "function") { return calls(); }
233
+ const settled = await Promise.allSettled(calls.map((c) => {
234
+ if (c && typeof c.then === "function") { return c; }
235
+ if (typeof c === "function") { return c(); }
236
+ if (typeof c === "object" && c !== null && (c.task || c.agent)) { return agent(c); }
237
+ return agent(c);
238
+ }));
239
+ return settled.map((r) => {
240
+ if (r.status === "fulfilled") {
241
+ const v = r.value;
242
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
243
+ // 主线程 fallback(postAgentResult/postResult serialization failed)回发的对象含 error 字段
244
+ // → 归一化为 failed 形状,与脚本侧 r.status === "failed" 检查统一
245
+ if (typeof v.error === "string" && v.error.length > 0) return { status: "failed", error: v.error };
246
+ return v;
247
+ }
248
+ return { status: "failed", error: "agent returned non-object result (type=" + typeof v + ")" };
249
+ }
250
+ const reason = r.reason;
251
+ const errMsg = reason instanceof Error ? reason.message : String(reason);
252
+ return { status: "failed", error: errMsg };
253
+ });
254
+ }
255
+
256
+ async function pipeline(firstArg, ...restStages) {
257
+ // Single-arg mode: pipeline([stage1, stage2, ...])
258
+ if (Array.isArray(firstArg) && restStages.length === 0) {
259
+ let result;
260
+ for (let i = 0; i < firstArg.length; i++) {
261
+ try { result = await firstArg[i](result); }
262
+ catch (e) {
263
+ const msg = e && e.message ? e.message : String(e);
264
+ _pushWorkerLog("error", ["[pipeline stage " + i + " failed]", msg]);
265
+ throw e;
266
+ }
267
+ }
268
+ return result;
269
+ }
270
+ // Cartesian product mode: pipeline([items], stage1, stage2, ...)
271
+ if (Array.isArray(firstArg) && restStages.length > 0 && typeof restStages[0] === "function") {
272
+ const results = [];
273
+ for (let idx = 0; idx < firstArg.length; idx++) {
274
+ const item = firstArg[idx];
275
+ let val = item;
276
+ let failed = false;
277
+ for (const stage of restStages) {
278
+ if (failed) break;
279
+ try { val = await stage(val); }
280
+ catch (e) {
281
+ const msg = e && e.message ? e.message : String(e);
282
+ _pushWorkerLog("error", ["[pipeline cartesian stage failed for item " + (idx + 1) + "]", msg]);
283
+ val = null; failed = true;
284
+ }
285
+ }
286
+ results.push(val);
287
+ }
288
+ return results;
289
+ }
290
+ throw new Error("pipeline() expects pipeline([stage1, ...]) or pipeline([items], stage1, ...)");
291
+ }
292
+
293
+ // ── workflow global — nested workflow invocation ──
294
+ async function workflow(name, args) {
295
+ if (typeof name !== "string" || name.length === 0) {
296
+ throw new Error("workflow() requires a workflow name string as first argument");
297
+ }
298
+ const workflowArgs = (typeof args === "object" && args !== null) ? args : {};
299
+ const callId = _callIdCounter;
300
+ _callIdCounter++;
301
+ if (!_safePost({ type: "workflow-call", callId, name, args: workflowArgs }, "workflow-call")) {
302
+ return Promise.reject(new Error("postMessage failed for workflow-call (name=" + name + "): see workerLogs"));
303
+ }
304
+ return new Promise((resolve, reject) => {
305
+ _pendingCalls.set(callId, { resolve, reject });
306
+ });
307
+ }
308
+
309
+ // ── User workflow script ──
310
+ // sample exercising template features
311
+ const name = $ARGS.name ?? "default";
312
+ phase("build");
313
+ const spec = await agent("build it", { label: "builder", schema: { type: "object" } });
314
+ const results = await parallel([
315
+ agent({ task: "unit tests", agent: "./tester.md" }),
316
+ agent({ task: "lint", agent: "./linter.md", skill: "lint-skill" }),
317
+ ]);
318
+ await pipeline([(x) => x, (x) => x]);
319
+ await workflow("deploy", { env: "prod" });
320
+ log("done " + name + " " + $WORKSPACE + " " + $BUDGET.remaining());
321
+ module.exports = { execute: async (ctx) => ctx.agent("finalize") };
322
+
323
+ // ── Auto-invoke execute() for module.exports pattern ──
324
+ if (typeof module !== "undefined" && module.exports && typeof module.exports.execute === "function") {
325
+ return await module.exports.execute({ agent, parallel, pipeline, phase, log, workflow, $ARGS, $WORKSPACE, $BUDGET });
326
+ }
327
+ })().then((result) => {
328
+ const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";
329
+ _safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return");
330
+ }).catch((err) => {
331
+ const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";
332
+ _safePost({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs }, "error");
333
+ });
@@ -93,7 +93,7 @@ describe("validateRunArgs — 校验语义", () => {
93
93
  const args = { targetType: "git-diff", target: "main", autoCommit: "false", maxRounds: "10" };
94
94
  const spec = makeSpec(reviewFixLoopParameters(), args);
95
95
  expect(() => validateRunArgs(spec)).not.toThrow();
96
- expect(spec.args).toBe(args); // 原地 mutate,引用不变(worker/resume 同一对象)
96
+ expect(spec.args).toBe(args); // 原地 mutate,引用不变(worker 启动/崩溃重跑同一对象)
97
97
  expect(spec.args.autoCommit).toBe(false);
98
98
  expect(spec.args.maxRounds).toBe(10);
99
99
  });