@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.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 +39 -12
  2. package/agents/analyst.md +61 -0
  3. package/agents/coder.md +70 -0
  4. package/agents/debugger.md +67 -0
  5. package/agents/doc-reviewer.md +3 -3
  6. package/agents/explorer.md +50 -18
  7. package/agents/general-purpose.md +19 -8
  8. package/agents/orchestrator.md +37 -32
  9. package/agents/planner.md +45 -11
  10. package/agents/researcher.md +53 -11
  11. package/agents/reviewer.md +74 -0
  12. package/package.json +1 -1
  13. package/skills/workflow-script-format/SKILL.md +1 -1
  14. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  15. package/src/execution/__tests__/agent-registry.test.ts +13 -11
  16. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  17. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  18. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  19. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  20. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  21. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  22. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  24. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  25. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  26. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  27. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  28. package/src/execution/__tests__/execution-record.test.ts +199 -0
  29. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  30. package/src/execution/__tests__/format.test.ts +131 -7
  31. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  32. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  33. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  34. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  35. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  36. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  37. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  38. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  39. package/src/execution/__tests__/list-component.test.ts +59 -5
  40. package/src/execution/__tests__/list-fields.test.ts +109 -0
  41. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  43. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  44. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  45. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  46. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  47. package/src/execution/__tests__/record-store.test.ts +158 -52
  48. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  49. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  50. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  51. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  52. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  53. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  54. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  55. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  56. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  57. package/src/execution/__tests__/session-pending.test.ts +61 -2
  58. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  59. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  60. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  61. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  62. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  63. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  64. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  65. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  66. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  67. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  69. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  70. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  71. package/src/execution/__tests__/tool-action.test.ts +12 -10
  72. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  73. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  74. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  75. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  76. package/src/execution/argv-mirror.ts +21 -2
  77. package/src/execution/execution-record.ts +126 -9
  78. package/src/execution/finalize-record.ts +90 -13
  79. package/src/execution/host-mode.ts +1 -1
  80. package/src/execution/lifecycle-manager.ts +484 -0
  81. package/src/execution/lifecycle-predicates.ts +65 -0
  82. package/src/execution/manifest-store.ts +61 -16
  83. package/src/execution/model-resolver.ts +26 -5
  84. package/src/execution/notifier.ts +69 -12
  85. package/src/execution/pi-invocation.ts +21 -1
  86. package/src/execution/record-store.ts +554 -107
  87. package/src/execution/session-pending.ts +116 -45
  88. package/src/execution/session-reconstructor.ts +224 -7
  89. package/src/execution/session-runner.ts +290 -75
  90. package/src/execution/sessions-index.ts +304 -0
  91. package/src/execution/stdin-writer.ts +93 -7
  92. package/src/execution/stream-sink.ts +20 -3
  93. package/src/execution/subagent-service.ts +867 -138
  94. package/src/execution/turn-limiter.ts +14 -0
  95. package/src/execution/types.ts +204 -22
  96. package/src/execution/worktree-manager.ts +128 -49
  97. package/src/execution/worktree-registry.ts +13 -2
  98. package/src/index.ts +277 -19
  99. package/src/injectors/subagent-list-injector.ts +26 -8
  100. package/src/injectors/workflow-list-injector.ts +25 -8
  101. package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
  102. package/src/interface/__tests__/tool-render.test.ts +15 -13
  103. package/src/interface/bg-notify-render.ts +32 -8
  104. package/src/interface/command-actions.ts +26 -7
  105. package/src/interface/commands.ts +21 -22
  106. package/src/interface/format.ts +44 -17
  107. package/src/interface/gui-mappers.ts +6 -8
  108. package/src/interface/helpers.ts +170 -10
  109. package/src/interface/list-component.ts +53 -14
  110. package/src/interface/subagent-actions.ts +235 -17
  111. package/src/interface/subagent-tool.ts +82 -17
  112. package/src/interface/subagents.ts +2 -1
  113. package/src/interface/tool-render.ts +11 -24
  114. package/src/interface/tool-workflow.ts +20 -35
  115. package/src/interface/views/WorkflowsView.ts +89 -32
  116. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  117. package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
  118. package/src/interface/views/format.ts +3 -3
  119. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  120. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  121. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  122. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  123. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  124. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  128. package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
  129. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  130. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  131. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  132. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  133. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  134. package/src/orchestration/agent-opts-resolver.ts +4 -1
  135. package/src/orchestration/args-validator.ts +2 -2
  136. package/src/orchestration/config-loader.ts +30 -1
  137. package/src/orchestration/error-recovery.ts +133 -29
  138. package/src/orchestration/execute-agent-call.ts +31 -7
  139. package/src/orchestration/jsonl-run-store.ts +287 -40
  140. package/src/orchestration/launcher.ts +7 -1
  141. package/src/orchestration/lifecycle.ts +135 -132
  142. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  143. package/src/orchestration/models/budget.ts +1 -1
  144. package/src/orchestration/models/run-runtime.ts +15 -17
  145. package/src/orchestration/models/run-spec.ts +2 -2
  146. package/src/orchestration/models/run-state.ts +3 -3
  147. package/src/orchestration/models/trace.ts +95 -15
  148. package/src/orchestration/models/types.ts +8 -9
  149. package/src/orchestration/models/workflow-run.ts +50 -71
  150. package/src/orchestration/models/workflow-script.ts +32 -1
  151. package/src/orchestration/skill-discovery.ts +30 -0
  152. package/src/orchestration/worker-handle.ts +1 -1
  153. package/src/orchestration/worker-host.ts +1 -1
  154. package/src/orchestration/worker-script-builder.ts +29 -10
  155. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  156. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  157. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  158. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  159. package/src/shared/agent-ref.ts +16 -0
  160. package/src/shared/resource-discovery.ts +147 -58
  161. package/src/shared/schema-jsonify.ts +53 -0
  162. package/workflows/README.md +4 -4
  163. package/agents/code-reviewer.md +0 -47
  164. package/agents/context-builder.md +0 -21
  165. package/agents/oracle.md +0 -34
  166. package/agents/worker.md +0 -20
@@ -17,9 +17,9 @@
17
17
  *
18
18
  * 关键实现点:(a) overlay 第二参数必须传,否则 view 不全屏;(b) trace 必须 per-render
19
19
  * 重读(run.state.trace.toArray 返回内部数组引用,trace.append 后下次 render 可见),
20
- * 1s tick invalidate + requestRender 保证运行中刷新;(c) 's' save 模式无条件可用
21
- * (saveWorkflow 内部对非 tmp workflow 返回错误消息);(d) pause/resume/abort 失败时
22
- * notify 反馈,不静默吞。
20
+ * 配条件失效 tick(签名比对,IF11)+ requestRender 保证运行中刷新;(c) 's' save 模式无条件可用
21
+ * (saveWorkflow 内部对非 tmp workflow 返回错误消息);(d) abort 失败时 notify 反馈,
22
+ * 不静默吞(run 一次性生命周期——无 pause/resume,abort 是唯一提前停止方式)。
23
23
  */
24
24
 
25
25
  import { promises as fsPromises } from "node:fs";
@@ -30,12 +30,14 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
30
30
  import { Key, matchesKey } from "@earendil-works/pi-tui";
31
31
 
32
32
  import {
33
+ countAllToolCalls,
33
34
  getAllToolCalls,
34
35
  projectLiveProgress,
35
36
  } from "../../execution/execution-record.ts";
36
37
  import type { ExecutionTraceNode } from "../../orchestration/models/types.ts";
37
38
  import type { WorkflowRun } from "../../orchestration/models/workflow-run.ts";
38
39
  import { saveWorkflow } from "../../orchestration/workflow-files.ts";
40
+ import { displayAgentName } from "../../shared/agent-ref.ts";
39
41
  import {
40
42
  BOX_BORDER_CHARS,
41
43
  BUDGET_TOKENS_DIVISOR,
@@ -122,14 +124,75 @@ interface TuiLike {
122
124
 
123
125
  /**
124
126
  * view 可触发的 lifecycle 操作(由调用方注入,避免 view 直接依赖 Engine 函数)。
125
- * 每个 action 接收 runId;调用方绑到 pauseRun/resumeRun/abortRun
127
+ * 每个 action 接收 runId;调用方绑到 abortRun(一次性生命周期——仅 abort)。
126
128
  */
127
129
  export interface ViewActions {
128
- pause: (runId: string) => Promise<void>;
129
- resume: (runId: string) => Promise<void>;
130
130
  abort: (runId: string) => Promise<void>;
131
131
  }
132
132
 
133
+ // ── 渲染签名(IF11/TC7/DM6:tick 条件失效的判据,渲染动态字段 SSOT)──────
134
+
135
+ /**
136
+ * computeRenderSignature — 渲染输出中全部动态决定字段的签名(now 参数化,供测试)。
137
+ *
138
+ * 纯度说明:now 是显式参数,但 live 节点的 elapsedSeconds 经 projectLiveProgress
139
+ * 内的 computeElapsedSeconds 现算(record.endedAt 缺省时取 Date.now())——即使两次
140
+ * 调用传同一 now,跨秒的真实时钟也会使签名变。该内含实时源只朝「多失效」方向
141
+ * 偏离(多失效、不多绘、更不漏绘),对「签名同 → 跳过重绘」的单向判据安全。
142
+ *
143
+ * 用途:200ms tick 用它与上次签名比对——相同则该帧内容无可见变化,跳过清缓存 +
144
+ * requestRender(纯等待场景零重建零重绘);不同则走现状失效路径。可见行为逐帧
145
+ * 等价的前提 = 签名字段集覆盖当前渲染的全部动态字段(漏字段 = 该字段渲染滞后一拍,
146
+ * ES7;签名取原始值时粒度细于显示量化值,只多失效不多绘不漏绘)。
147
+ *
148
+ * **字段核对表**(每字段 ↔ 渲染消费点 file:line,完备性唯一证据——测试只能证
149
+ * 「已入字段变 → 签名变」,不能证无漏字段):
150
+ *
151
+ * | 签名字段 | 消费点 |
152
+ * |---|---|
153
+ * | run.state.status | WorkflowsView.ts renderHeader :652 / detail-content.ts :75(statusDotStr+statusLabel)/ renderFooter :695-699 |
154
+ * | 秒桶 Math.floor(now/1000) | renderHeader :651 formatElapsed(现算 elapsed)+ detail-content.ts :69-72(now 参与未完成节点 elapsed)|
155
+ * | completed/total(节点 status 推导)| renderHeader :649-652 |
156
+ * | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与 :654 同精度——第 3-4 位小数变化是可见变化)| renderHeader :654 / saveTraceToFile :942 |
157
+ * | run.state.errorLogs 指纹 = length+末条 level:message(errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异——append-only + 前向淘汰、条目不可变;封顶后 length 不变内容移,单取 length 会漏失效,而任何可见变化必伴随 length 变或末条变,故 length+末条是完备且最小的指纹)| detail-content.ts :174-191(renderWorkerLogSection:total 标签 + 末 20 条)|
158
+ * | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile :947(trace 导出 `### [#stepIndex]` 标题)|
159
+ * | 节点 sessionFile | detail-content.ts :314-317(renderSessionSection)|
160
+ * | 节点 status | 节点行 :832 statusDotStr / detail :75 |
161
+ * | live.totalTokens | 节点行 :836 / detail :79 / :275 |
162
+ * | 工具计数(签名路径用 countAllToolCalls 免克隆计数,与 getAllToolCalls(node.live).length 恒等——projectLiveProgress 投影无 toolCallCount 字段,不得引用)| 节点行 :837 / detail :80 / :223 |
163
+ * | live.elapsedSeconds | 节点行 :838 / detail :81 / :276 |
164
+ * | live.turns | detail :224 / :276 |
165
+ * | live.eventLog.length(append-only,长度变化即尾部窗口右移出新事件)| detail :222(filter turn_end)+ :231-235 |
166
+ * | live.currentActivity(type+label 均入签名)| detail :227-228 |
167
+ * | live.lastError(内容入签名,防同存在性不同文本漏绘)| detail :277-278 |
168
+ *
169
+ * 维护约定(DS8):WorkflowsView/detail-content 新增任何动态展示字段必须同步
170
+ * 并入本签名并更新上表,否则该字段渲染滞后一拍。本文件编辑会使表内 WorkflowsView
171
+ * 行号漂移——改动核对表下方代码后必须 grep 实际位置刷新本表。
172
+ */
173
+ /** 秒桶宽度(ms):签名取 Math.floor(now / SECOND_MS),秒桶推进 = 可见 elapsed 变化。 */
174
+ const SECOND_MS = 1000;
175
+
176
+ export function computeRenderSignature(run: WorkflowRun, now: number): string {
177
+ const traceArr = run.state.trace.toArray();
178
+ const completed = traceArr.filter((n) => n.status === "completed").length;
179
+ const budget = run.state.budget;
180
+ // budget 量化串与 renderHeader budgetStr 同源同精度(round-k token / toFixed(4) cost)
181
+ const budgetPart = `${Math.round(budget.usedTokens / BUDGET_TOKENS_DIVISOR)}k/${budget.maxTokens ? `${Math.round(budget.maxTokens / BUDGET_TOKENS_DIVISOR)}k` : "∞"}::$${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`;
182
+ // errorLogs 指纹:detail 只渲染 total 标签 + 末 20 条(renderWorkerLogSection),
183
+ // 而 errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异(append-only + 前向淘汰、
184
+ // 条目不可变)——封顶后 length 不变内容移,单取 length 漏失效;任何可见变化必
185
+ // 伴随「length 变」或「末条变」,故 length+末条(level:message)是完备最小指纹。
186
+ const logs = run.state.errorLogs;
187
+ const lastLog = logs && logs.length > 0 ? `${logs[logs.length - 1].level}:${logs[logs.length - 1].message}` : "-";
188
+ const errorLogsPart = `${logs?.length ?? 0}:${lastLog}`;
189
+ const nodeParts = traceArr.map((n) => {
190
+ const live = n.live ? projectLiveProgress(n.live) : undefined;
191
+ return `${n.stepIndex}:${n.status}:${n.sessionFile ?? "-"}:${live?.totalTokens ?? -1}:${n.live ? countAllToolCalls(n.live) : -1}:${live?.elapsedSeconds ?? -1}:${live?.turns ?? -1}:${live?.eventLog.length ?? -1}:${live?.currentActivity ? `${live.currentActivity.type}:${live.currentActivity.label}` : "-"}:${live?.lastError ?? "-"}`;
192
+ });
193
+ return [run.state.status, Math.floor(now / SECOND_MS), `${completed}/${traceArr.length}`, budgetPart, errorLogsPart, ...nodeParts].join("|");
194
+ }
195
+
133
196
  // ── View state ────────────────────────────────────────────────
134
197
 
135
198
  interface ViewState {
@@ -181,7 +244,7 @@ function createInitialState(): ViewState {
181
244
  * @param run WorkflowRun 聚合根(读 state.status/spec/trace/meta)
182
245
  * @param theme ThemeLike(避免直接 import Pi runtime)
183
246
  * @param ctx ExtensionContext(调 ui.custom 渲染 + ui.notify 错误反馈)
184
- * @param actions lifecycle 操作(pause/resume/abort),由调用方注入
247
+ * @param actions lifecycle 操作(abort),由调用方注入
185
248
  */
186
249
  export function createWorkflowsView(
187
250
  run: WorkflowRun,
@@ -212,10 +275,17 @@ export function createWorkflowsView(
212
275
  const requestRender = () => tui.requestRender();
213
276
 
214
277
  // ── 轮询 tick:engine 无事件推送,view 自轮询 trace 变化 ──
215
- // 每 200ms 重绘,保证 header 动态数据(elapsed/tokens)实时更新。
216
- // 行数固定后,diff-redraw 引擎能正确逐行对比,不会出现残影。
278
+ // 每 200ms 条件失效(IF11/TC7):先算渲染签名(computeRenderSignature,
279
+ // 覆盖 header/节点行/L2 detail 全部动态字段),与上次相同 → 该帧内容无可见
280
+ // 变化,直接 return(不清 cache、不 requestRender——纯等待场景零重建零重绘);
281
+ // 不同 → 现状路径(清缓存 + requestRender)。lastSignature 初始 undefined,
282
+ // 首 tick 必失效(保证首绘)。签名计算 O(N)(N=trace 节点数)远低于全量 render。
283
+ let lastSignature: string | undefined;
217
284
  const tick = setInterval(() => {
218
285
  if (state.disposed) return;
286
+ const signature = computeRenderSignature(run, Date.now());
287
+ if (signature === lastSignature) return;
288
+ lastSignature = signature;
219
289
  cache.key = undefined;
220
290
  cache.lines = undefined;
221
291
  requestRender();
@@ -358,21 +428,9 @@ export function createWorkflowsView(
358
428
  return;
359
429
  }
360
430
 
361
- // ── Lifecycle shortcuts (no restart per D-9) ──
362
- if (data === "p") {
363
- if (run.state.status === "running") {
364
- void actions.pause(run.runId)
365
- .then(() => { cache.key = undefined; requestRender(); })
366
- .catch((err: Error) => ctx.ui.notify(`Pause failed: ${err.message}`, "error"));
367
- } else if (run.state.status === "paused") {
368
- void actions.resume(run.runId)
369
- .then(() => { cache.key = undefined; requestRender(); })
370
- .catch((err: Error) => ctx.ui.notify(`Resume failed: ${err.message}`, "error"));
371
- }
372
- return;
373
- }
431
+ // ── Lifecycle shortcuts (no restart per D-9; no pause/resume — one-shot) ──
374
432
  if (data === "a") {
375
- if (run.state.status === "running" || run.state.status === "paused") {
433
+ if (run.state.status === "running") {
376
434
  void actions.abort(run.runId)
377
435
  .then(() => { cache.key = undefined; requestRender(); })
378
436
  .catch((err: Error) => ctx.ui.notify(`Abort failed: ${err.message}`, "error"));
@@ -498,7 +556,7 @@ export function createWorkflowsView(
498
556
  // │ ● 2 deploy 0/1 │ ● tester model ... │
499
557
  // │ (pad) │ (pad) │
500
558
  // ╰───────────────────────────────────────────────────╯
501
- // ↑↓ phase · ⏎ enter · p pause · a abort · s save · esc back ← footer (框外)
559
+ // ↑↓ phase · ⏎ enter · a abort · s save · esc back ← footer (框外)
502
560
  //
503
561
  // body = sidebar(SIDEBAR_WIDTH) │ main(rest)
504
562
  // save overlay 活跃时居中覆盖 body。
@@ -621,10 +679,8 @@ function renderFooter(
621
679
  ? "↑↓ agent · ⏎ detail"
622
680
  : "↑↓ agent · ⏎ prompt · PgUp/PgDn scroll";
623
681
  const actionParts: string[] = [];
624
- const status = run.state.status;
625
- if (status === "running" || status === "paused") {
682
+ if (run.state.status === "running") {
626
683
  actionParts.push("a abort");
627
- actionParts.push(status === "paused" ? "p resume" : "p pause");
628
684
  }
629
685
  actionParts.push("s save");
630
686
  actionParts.push("S trace");
@@ -765,7 +821,7 @@ function renderLevel1(
765
821
  const tokStr = live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "";
766
822
  const tcCount = getAllToolCalls(node.live).length;
767
823
  const elapsed = formatElapsedSeconds(live.elapsedSeconds);
768
- rightLines.push(`${pointer}${dot} ${node.agent} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
824
+ rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
769
825
  } else {
770
826
  const elapsed = formatElapsed(
771
827
  node.startedAt,
@@ -774,7 +830,7 @@ function renderLevel1(
774
830
  const tok = node.result?.usage;
775
831
  const tokStr = tok ? `${Math.round((tok.input + tok.output) / BUDGET_TOKENS_DIVISOR)}k tok` : "";
776
832
  const tcCount = node.result?.toolCalls?.length ?? 0;
777
- rightLines.push(`${pointer}${dot} ${node.agent} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
833
+ rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
778
834
  }
779
835
  }
780
836
  state.agentScrollOffset = agentStart;
@@ -808,9 +864,10 @@ function renderLevel2(
808
864
  const a = agents[i];
809
865
  const pointer = i === state.agentIdx ? "❯ " : " ";
810
866
  const maxNameWidth = SIDEBAR_WIDTH - AGENT_NAME_BUDGET;
811
- const agentName = visibleLen(a.agent) > maxNameWidth
812
- ? a.agent.slice(0, maxNameWidth - 1) + ELLIPSIS
813
- : a.agent;
867
+ const agentRef = displayAgentName(a.agent);
868
+ const agentName = visibleLen(agentRef) > maxNameWidth
869
+ ? agentRef.slice(0, maxNameWidth - 1) + ELLIPSIS
870
+ : agentRef;
814
871
  leftLines.push(`${pointer}${agentName}`);
815
872
  }
816
873
 
@@ -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
+ });
@@ -21,7 +21,7 @@ const plainTheme: ThemeLike = {
21
21
  function makeNode(overrides: Partial<ExecutionTraceNode> = {}): ExecutionTraceNode {
22
22
  return {
23
23
  stepIndex: 0,
24
- agent: "worker",
24
+ agent: "coder",
25
25
  task: "do something",
26
26
  model: "default",
27
27
  status: "completed",
@@ -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(" ");