@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
package/README.md CHANGED
@@ -57,6 +57,21 @@ pi --tools todo,goal_control,workflow,subagent,ask_user \
57
57
 
58
58
  系统内置 `n = 10` 深度护栏(fork 链 + 嵌套取 max),超过抛 `ForkDepthExceededError`。实测建议控制在 **3-4 层**以内——更深层会因上下文逐层压缩导致信息失真。
59
59
 
60
+ ## Workflow 生命周期(one-shot)
61
+
62
+ Workflow run 是一次性执行,状态机两态:`running → done`(`done` 唯一终态,reason 区分 completed / aborted / failed / budget_limited / time_limited)。`workflow` tool 仅 3 个 action:`run` / `status` / `abort`。
63
+
64
+ Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.
65
+
66
+ - **abort 是唯一的提前停止方式**:`{"action":"abort","runId":"<id>"}`(可选 `"error":"<reason>"`)。pause/resume action 已移除,调用会被 pi schema 校验拒绝(`Validation failed for tool "workflow"`);`/workflows pause|resume <id>` 返回 removed 提示
67
+ - **session 切换/关闭时**,所有 running run 当即作废转 `done,failed`(state.error 为 `Session switched: run terminated` / `Session shutdown: run terminated`),已投入的 token 作废;需要结果就重新 run
68
+ - **快照格式 `wf-run-v2`**(status 两态、无 `pausedAt`);旧 `wf-run-v1` 文件加载时静默跳过
69
+ - **worker 崩溃自动重建重试**(默认 3 次):重建时在飞 agent 调用被清除重跑,已完成的调用保留 replay 缓存,不重复消耗 token
70
+
71
+ ## 性能:sessions-index.json 持久化索引
72
+
73
+ 冷启动首扫的 identity 探测结论持久化为 `<enc>/sessions-index.json`(stat 戳自校验、tmp(pid)+rename 原子写、60s 节流、损坏/版本不符静默回退全量探测),真实目录(1744 jsonl / 671MB)实测冷扫描中位数 972.8ms → 80.6ms(12.1x,预算 ≤300ms)。可复现验收脚本:`bench/cold-scan.bench.ts`(冷扫描计时 + 输出等价断言)、`bench/concurrent-scan.bench.ts`(3 实例并发 + 随机变异四判定),设计文档见 `.xyz-harness/2026-08-15-subagent-workflow-perf/sessions-index-design.md`。
74
+
60
75
  ## 安装
61
76
 
62
77
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "7.4.0",
3
+ "version": "8.1.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -22,6 +22,7 @@
22
22
  "agents/",
23
23
  "skills/",
24
24
  "workflows/",
25
+ "scripts/",
25
26
  "src/index.ts",
26
27
  "src/execution/",
27
28
  "src/injectors/",
@@ -41,17 +42,18 @@
41
42
  ]
42
43
  },
43
44
  "dependencies": {
44
- "@xyz-agent/extension-protocol": "^0.4.0",
45
45
  "ajv": "^8.20.0",
46
46
  "yaml": "^2.9.0",
47
- "@zhushanwen/pi-extension-logger": "0.2.1"
47
+ "@xyz-agent/extension-protocol": "0.5.0",
48
+ "@zhushanwen/pi-extension-logger": "0.2.1",
49
+ "@zhushanwen/pi-file-lock": "0.1.1"
48
50
  },
49
51
  "peerDependencies": {
50
52
  "@earendil-works/pi-ai": "*",
51
53
  "@earendil-works/pi-coding-agent": "*",
52
54
  "@earendil-works/pi-tui": "*",
53
55
  "typebox": "*",
54
- "@zhushanwen/pi-pending-notifications": "0.3.2",
56
+ "@zhushanwen/pi-pending-notifications": "0.3.3",
55
57
  "@zhushanwen/pi-structured-output": "5.0.1"
56
58
  },
57
59
  "peerDependenciesMeta": {
@@ -64,6 +66,7 @@
64
66
  },
65
67
  "devDependencies": {
66
68
  "@types/node": "^24.0.0",
69
+ "@vitest/coverage-v8": "^4.1.9",
67
70
  "vitest": "^4.1.8"
68
71
  },
69
72
  "scripts": {
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ // rfl.mjs — review-fix-loop run 数据查询 CLI(tier-1 §7.4,零依赖 node 脚本)
3
+ //
4
+ // 读取 ~/.review-fix-loop/<repo-slug>/<runId>/state.json(workflow 脚本落盘),
5
+ // 派生指标现算不落盘。M0 数据结构(calls/batches[].rounds[].phaseTimings)为全集,
6
+ // M1/M2 字段(issues[].origin/dormant/scores)缺省容错显示。
7
+ //
8
+ // 用法:
9
+ // node rfl.mjs list [repoSlug] run 清单(runId/时间/终止原因/轮数)
10
+ // node rfl.mjs stats <runId|latest> 单 run 全景(token/缓存/per-role/轮次时间线)
11
+ // node rfl.mjs trends [repoSlug] 跨 run 趋势表
12
+ // node rfl.mjs clean --older-than 30d [--yes] 清理(默认干跑)
13
+ import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ const ROOT = join(homedir(), ".review-fix-loop");
18
+
19
+ function isDir(p) {
20
+ try { return statSync(p).isDirectory(); } catch { return false; }
21
+ }
22
+
23
+ function listRuns(repoSlug) {
24
+ if (!isDir(ROOT)) return [];
25
+ const repos = repoSlug ? [repoSlug] : readdirSync(ROOT).filter((d) => isDir(join(ROOT, d)));
26
+ const runs = [];
27
+ for (const repo of repos) {
28
+ const repoDir = join(ROOT, repo);
29
+ if (!isDir(repoDir)) continue;
30
+ for (const runId of readdirSync(repoDir)) {
31
+ const stateFile = join(repoDir, runId, "state.json");
32
+ if (!existsSync(stateFile)) continue;
33
+ try {
34
+ runs.push({ repo, runId, state: JSON.parse(readFileSync(stateFile, "utf8")), stateFile });
35
+ } catch { /* 损坏 state 跳过(半写入 run) */ }
36
+ }
37
+ }
38
+ return runs.sort((a, b) => String(a.state?.meta?.startedAt ?? "").localeCompare(String(b.state?.meta?.startedAt ?? "")));
39
+ }
40
+
41
+ function fmtTokens(n) {
42
+ if (n == null) return "-";
43
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
44
+ if (n >= 1_000) return Math.round(n / 1_000) + "k";
45
+ return String(n);
46
+ }
47
+
48
+ function fmtMs(ms) {
49
+ if (ms == null) return "-";
50
+ if (ms >= 60_000) return Math.floor(ms / 60_000) + "m" + Math.round((ms % 60_000) / 1000) + "s";
51
+ if (ms >= 1_000) return Math.round(ms / 1000) + "s";
52
+ return Math.round(ms) + "ms";
53
+ }
54
+
55
+ function fmtCost(c) {
56
+ if (c == null) return "-";
57
+ return "$" + (c >= 0.01 ? c.toFixed(2) : c.toFixed(4));
58
+ }
59
+
60
+ /** usage 容错取数:usage 缺失或字段缺省一律按 0 计。 */
61
+ function usageOrZero(u, k) {
62
+ return (u && u[k]) || 0;
63
+ }
64
+
65
+ /** 累加单个 call 的 token 四分量与 cost(无 usage 的 call 跳过)。 */
66
+ function addUsageSums(sum, u) {
67
+ if (!u) return;
68
+ sum.input += u.input || 0;
69
+ sum.output += u.output || 0;
70
+ sum.cacheRead += u.cacheRead || 0;
71
+ sum.cacheWrite += u.cacheWrite || 0;
72
+ sum.cost += u.cost || 0;
73
+ }
74
+
75
+ /** 聚合 calls[]:token 四分量/cost/命中率/per-role。 */
76
+ function summarizeCalls(calls) {
77
+ const sum = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
78
+ const byRole = {};
79
+ for (const c of calls || []) {
80
+ const u = c.usage;
81
+ addUsageSums(sum, u);
82
+ const key = c.role || "unknown";
83
+ byRole[key] = byRole[key] || { count: 0, input: 0, cacheRead: 0 };
84
+ byRole[key].count++;
85
+ byRole[key].input += usageOrZero(u, "input");
86
+ byRole[key].cacheRead += usageOrZero(u, "cacheRead");
87
+ }
88
+ const denom = sum.input + sum.cacheRead;
89
+ const cachePct = denom > 0 ? Math.round((sum.cacheRead / denom) * 100) : null;
90
+ return { sum, byRole, cachePct };
91
+ }
92
+
93
+ function roundCount(state) {
94
+ return (state.batches || []).reduce((a, b) => a + (b.rounds || []).length, 0);
95
+ }
96
+
97
+ /** phaseTimings 值合法性:[startMs, endMs] 数字对(半写入 run 容错)。 */
98
+ function isTimingPair(pair) {
99
+ return Array.isArray(pair) && typeof pair[0] === "number" && typeof pair[1] === "number";
100
+ }
101
+
102
+ /** 收集 run 内全部合法 phase 时序对 [startMs, endMs](非法值=半写入 run 容错跳过)。 */
103
+ function validTimingPairs(state) {
104
+ const pairs = [];
105
+ for (const b of state.batches || []) {
106
+ for (const r of b.rounds || []) {
107
+ for (const pair of Object.values(r.phaseTimings || {})) {
108
+ if (isTimingPair(pair)) pairs.push(pair);
109
+ }
110
+ }
111
+ }
112
+ return pairs;
113
+ }
114
+
115
+ /** run 墙钟时间 = 全部合法时序对的 min(start) → max(end);无任何合法对返回 null。 */
116
+ function wallMs(state) {
117
+ const pairs = validTimingPairs(state);
118
+ if (pairs.length === 0) return null;
119
+ return Math.max(...pairs.map((p) => p[1])) - Math.min(...pairs.map((p) => p[0]));
120
+ }
121
+
122
+ function cmdList(repoSlug) {
123
+ const runs = listRuns(repoSlug);
124
+ if (runs.length === 0) {
125
+ console.log("no runs found" + (repoSlug ? " for repo " + repoSlug : "") + " (root: " + ROOT + ")");
126
+ return 0;
127
+ }
128
+ for (const r of runs) {
129
+ const started = (r.state.meta && r.state.meta.startedAt) || "?";
130
+ const term = (r.state.meta && r.state.meta.terminated) || "?";
131
+ console.log(
132
+ r.runId.padEnd(28) + " " + String(started).padEnd(25) +
133
+ " " + String(term).padEnd(18) +
134
+ " rounds: " + roundCount(r.state) +
135
+ (r.state.calls ? " calls: " + r.state.calls.length : ""),
136
+ );
137
+ }
138
+ return 0;
139
+ }
140
+
141
+ /** stats 概览段:terminated/轮数 + token 四分量 + per-role 明细。 */
142
+ function printStatsSummary(s, { sum, cachePct, byRole }) {
143
+ console.log(" terminated: " + ((s.meta && s.meta.terminated) || "?") + " rounds: " + roundCount(s) + " started: " + (s.meta && s.meta.startedAt));
144
+ console.log(
145
+ " tokens: input " + fmtTokens(sum.input) +
146
+ " (cacheRead " + (cachePct == null ? "-" : cachePct + "%") + ")" +
147
+ " output " + fmtTokens(sum.output) +
148
+ " cost " + fmtCost(sum.cost) +
149
+ " wall " + fmtMs(wallMs(s)),
150
+ );
151
+ const roleParts = Object.entries(byRole).map(([role, v]) =>
152
+ role + " ×" + v.count + " " + fmtTokens(v.input + v.cacheRead));
153
+ console.log(" per-role: " + (roleParts.join(" │ ") || "(no calls)"));
154
+ }
155
+
156
+ /** stats issues 概览段(M1 origin/dormant 字段缺省容错)。 */
157
+ function printStatsIssues(s) {
158
+ const issues = s.issues ? Object.values(s.issues) : [];
159
+ const fixed = issues.filter((i) => i.status === "fixed").length;
160
+ const regressed = issues.filter((i) => (i.history || []).some((h) => h.status === "regressed")).length;
161
+ const origins = {};
162
+ for (const i of issues) origins[i.origin || "-"] = (origins[i.origin || "-"] || 0) + 1;
163
+ // issues 与 dormant 都是批作用域(state.issues 每批重置,最终 state 只含末批
164
+ // 条目,非 run 累计)——表述显式限定作用域,防止被误读为整个 run 的总数
165
+ console.log(" issues (last batch): total " + issues.length + " → fixed " + fixed +
166
+ " regressed-ever " + regressed +
167
+ " origins " + (issues.length ? Object.entries(origins).map(([k, v]) => k + " " + v).join("/") : "-") +
168
+ " dormant (last batch) " + ((s.dormant || []).length));
169
+ }
170
+
171
+ /** stats scores 表段(M2 字段缺省容错)。 */
172
+ function printStatsScores(s) {
173
+ if (Array.isArray(s.scores) && s.scores.length > 0) {
174
+ for (const sc of s.scores) {
175
+ // regression=null = 该维度 unverifiable(workflow 无对账数据可回填),无分值
176
+ // 语义——显示 n/a 而非 JS 字面量 "null"(与 total 缺省 "(no total)" 惯例一致)
177
+ const dims = sc.dimensions ? Object.entries(sc.dimensions).map(([k, v]) => k + " " + (v == null ? "n/a" : v)).join(" ") : "";
178
+ // scores entry 的 round 是批局部编号(每批从 1 重新计数),多批 run 下批 1 与
179
+ // 批 2 的 "R1" 两行完全同形无法归批——行首补 B<batch> 标识(与下方时间线
180
+ // R<n> (batch N) 的批标注口径呼应);旧数据无 batch 字段回退 B1
181
+ console.log(" score B" + (sc.batch ?? 1) + " R" + (sc.round ?? "?") + " " + (sc.targetKind || "?") + "/" + (sc.targetName || "?") +
182
+ ": " + (sc.total != null ? sc.total + "/10" : "(no total)") + " [" + dims + "]");
183
+ }
184
+ } else {
185
+ console.log(" scores: (none)");
186
+ }
187
+ }
188
+
189
+ /** stats 轮次时间线段。 */
190
+ function printStatsTimeline(s) {
191
+ for (const b of s.batches || []) {
192
+ for (const r of b.rounds || []) {
193
+ const pt = r.phaseTimings || {};
194
+ const seg = (k) => (Array.isArray(pt[k]) ? k + " " + fmtMs(pt[k][1] - pt[k][0]) : k + " -");
195
+ console.log(" R" + r.round + " (batch " + b.index + "): " + seg("review") + " │ " + seg("aggregate") + " │ " + seg("fix") +
196
+ " mustFix " + (r.mustFix ?? "-"));
197
+ }
198
+ }
199
+ }
200
+
201
+ function cmdStats(runIdOrLatest, repoSlug) {
202
+ const runs = listRuns(repoSlug);
203
+ if (runs.length === 0) { console.log("no runs found (root: " + ROOT + ")"); return 1; }
204
+ const run = runIdOrLatest === "latest" ? runs[runs.length - 1] : runs.find((r) => r.runId === runIdOrLatest);
205
+ if (!run) { console.log("run not found: " + runIdOrLatest); return 1; }
206
+ const s = run.state;
207
+ const { sum, byRole, cachePct } = summarizeCalls(s.calls);
208
+ console.log("run " + run.runId + " (repo: " + run.repo + ")");
209
+ printStatsSummary(s, { sum, cachePct, byRole });
210
+ printStatsIssues(s);
211
+ printStatsScores(s);
212
+ printStatsTimeline(s);
213
+ return 0;
214
+ }
215
+
216
+ function cmdTrends(repoSlug) {
217
+ const runs = listRuns(repoSlug);
218
+ if (runs.length === 0) { console.log("no runs found (root: " + ROOT + ")"); return 0; }
219
+ // started 列宽 = ISO 8601 恒长 24(padEnd(20) 会让表头比数据行左移 4 字符错位)
220
+ // reg/att 列名刻意缩短:该列是末批作用域(见循环后 legend),长名 "regressed/
221
+ // attempted" 会被读成与 rounds/tokens/cache% 同级的 run 级指标
222
+ const header = "runId".padEnd(28) + " " + "started".padEnd(24) + " " + "rounds".padEnd(6) + " " + "tokens".padEnd(8) + " " + "cache%".padEnd(6) + " " + "reg/att";
223
+ console.log(header);
224
+ for (const r of runs) {
225
+ const { sum, cachePct } = summarizeCalls(r.state.calls);
226
+ const issues = r.state.issues ? Object.values(r.state.issues) : [];
227
+ const regressed = issues.filter((i) => (i.history || []).some((h) => h.status === "regressed")).length;
228
+ // 分母 = 曾进入 fix-attempted 的 issue 数,与分子(含 regressed history 的 issue
229
+ // 数)同为 issue 计数口径(§6.7「fix 质量 = 1 − regressed/fix-attempted」的分母
230
+ // 应是被尝试修复过的 issue)。不能用 Σ fixAttempts:该字段权威语义是「修复失败
231
+ // 次数」(初始 0,每次转 regressed 才 +1),5 个 issue 修 4 成 1 败会算成 1/1
232
+ // (字面 100% 回归率),正确口径是 1/5;进入过修复的痕迹只有 history 的
233
+ // fix-attempted 条目。分母 0(从未尝试修复)率无意义,显示 "-"。
234
+ const attempted = issues.filter((i) => (i.history || []).some((h) => h.status === "fix-attempted")).length;
235
+ const regressedCol = attempted > 0 ? regressed + "/" + attempted : "-";
236
+ console.log(
237
+ r.runId.padEnd(28) + " " + String((r.state.meta && r.state.meta.startedAt) || "?").padEnd(24) + " " +
238
+ String(roundCount(r.state)).padEnd(6) + " " +
239
+ fmtTokens(sum.input + sum.cacheRead + sum.output).padEnd(8) + " " +
240
+ String(cachePct == null ? "-" : cachePct + "%").padEnd(6) + " " + regressedCol,
241
+ );
242
+ }
243
+ // reg/att 与同行其他列口径不同:state.issues 每批重置,最终 state 只含末批条目
244
+ // (分子分母内部一致),而 rounds/tokens/cache% 是 run 级累计——跨 run 对比时
245
+ // 易把末批数据误读为 run 级,表后用 legend 显式标注作用域与分母口径(函数开头
246
+ // 已保证 runs 非空,走到这里表必然非空)
247
+ console.log("reg/att: last-batch scope (issues reset per batch); denominator = issues ever fix-attempted");
248
+ return 0;
249
+ }
250
+
251
+ function parseOlderThan(spec) {
252
+ const m = /^(\d+)([dh])$/.exec(String(spec || ""));
253
+ if (!m) return null;
254
+ return Date.now() - parseInt(m[1], 10) * (m[2] === "d" ? 86_400_000 : 3_600_000);
255
+ }
256
+
257
+ function cmdClean(spec, yes) {
258
+ const cutoff = parseOlderThan(spec);
259
+ if (cutoff == null) {
260
+ console.log("invalid --older-than (expect <N>d or <N>h, e.g. 30d)");
261
+ return 1;
262
+ }
263
+ const runs = listRuns();
264
+ const expired = runs.filter((r) => {
265
+ const t = r.stateFile ? statSync(r.stateFile).mtimeMs : 0;
266
+ const started = r.state.meta && r.state.meta.startedAt ? Date.parse(r.state.meta.startedAt) : NaN;
267
+ const basis = Number.isFinite(started) ? started : t;
268
+ return basis < cutoff;
269
+ });
270
+ if (expired.length === 0) { console.log("nothing older than " + spec); return 0; }
271
+ for (const r of expired) {
272
+ const dir = join(ROOT, r.repo, r.runId);
273
+ if (yes) {
274
+ rmSync(dir, { recursive: true, force: true });
275
+ console.log("deleted " + dir);
276
+ } else {
277
+ console.log("would delete " + dir + " (dry-run; add --yes to execute)");
278
+ }
279
+ }
280
+ return 0;
281
+ }
282
+
283
+ // ── main ──────────────────────────────────────────────────────
284
+ const [cmd, ...rest] = process.argv.slice(2);
285
+ try {
286
+ switch (cmd) {
287
+ case "list": process.exit(cmdList(rest[0])); break;
288
+ case "stats": {
289
+ if (!rest[0]) { console.log("usage: rfl stats <runId|latest> [repoSlug]"); process.exit(1); }
290
+ process.exit(cmdStats(rest[0], rest[1]));
291
+ break;
292
+ }
293
+ case "trends": process.exit(cmdTrends(rest[0])); break;
294
+ case "clean": {
295
+ const oi = rest.indexOf("--older-than");
296
+ const spec = oi >= 0 ? rest[oi + 1] : null;
297
+ if (!spec) { console.log("usage: rfl clean --older-than <N>d [--yes]"); process.exit(1); }
298
+ process.exit(cmdClean(spec, rest.includes("--yes")));
299
+ break;
300
+ }
301
+ default:
302
+ console.log("usage: rfl <list|stats|trends|clean> ...\n list [repoSlug]\n stats <runId|latest> [repoSlug]\n trends [repoSlug]\n clean --older-than 30d [--yes]");
303
+ process.exit(cmd ? 1 : 0);
304
+ }
305
+ } catch (err) {
306
+ console.error("rfl: " + (err && err.message ? err.message : String(err)));
307
+ process.exit(2);
308
+ }
@@ -241,7 +241,7 @@ agent({ prompt: '...', description: 'review-business-logic' });
241
241
 
242
242
  ## Constraints
243
243
 
244
- - `agent()` calls **must be deterministic in order** for pause/resume to work correctly. 根因:调用结果按单调递增的 callId(从 0 起、按调用顺序)缓存,pause 时杀 Worker 但保留 callCache,resume 时按 callId 重放。`parallel()` 内的调用顺序不能随机,否则重放会错位命中旧结果。注意:无 script hash 校验,**改脚本后 resume 会用旧结果**,开发期改脚本应重新 run。
244
+ - `agent()` calls **must be deterministic in order** for crash-retry replay to work correctly. 根因:调用结果按单调递增的 callId(从 0 起、按调用顺序)缓存。Runs are one-shot(无 pause/resume——提前停止用 abort,要新结果重新 run);worker 基础设施错误自动重试 ≤3 次,重试时已完成调用按 callId 重放缓存结果不重跑。`parallel()` 内的调用顺序不能随机,否则重放会错位命中旧结果。注意:无 script hash 校验,重试重放的是缓存结果,开发期改脚本应重新 run。
245
245
  - `parallel()` 并发默认上限 6(ConcurrencyPool,超出自动排队;来源 ADR-030 决策 3)。
246
246
  - Throwing an error aborts the workflow (after retries).
247
247
  - Use `require()` for Node.js built-ins: `const fs = require('node:fs');`
@@ -0,0 +1 @@
1
+ [{"label":"plain-ascii-long","maxWidth":40,"text":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expected":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…"},{"label":"cjk-long","maxWidth":33,"text":"你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界","expected":"你好世界你好世界你好世界你好世界…"},{"label":"sgr-mixed-10k","maxWidth":60,"text":"汉字测试 data \u001b[0m\u001b[38;5;9m汉字测试 \u001b[mbeta \u001b[0mγλυφ \u001b[0malpha alpha \u001b[31m\u001b[38;5;9malpha \u001b[mdata \u001b[Kalpha \u001b[0mbeta beta \u001b[K→arrow beta \u001b[1;32m\u001b[38;5;9mdata alpha 汉字测试 alpha \u001b[31m\u001b]0;title\u0007γλυφ alpha →arrow \u001b[38;5;9m\u001b[0malpha γλυφ 汉字测试 汉字测试 beta beta →arrow →arrow 汉字测试 beta \u001b[38;5;9m\u001b[K→arrow \u001b[0malpha beta data \u001b[0m\u001b[38;5;9m\u001b[K\u001b[38;5;9mbeta 汉字测试 \u001b[m\u001b]0;title\u0007\u001b[38;5;9m→arrow beta 汉字测试 \u001b[31mdata \u001b[1;32m\u001b[K\u001b[1;32mγλυφ alpha \u001b[0m\u001b[1;32m汉字测试 汉字测试 \u001b[Kbeta γλυφ γλυφ \u001b[1;32m\u001b[1;32mbeta \u001b[38;5;9m\u001b[K\u001b[1;32m\u001b[0m\u001b[0m\u001b]0;title\u0007→arrow 汉字测试 \u001b[m→arrow 汉字测试 \u001b[38;5;9mbeta \u001b]0;title\u0007→arrow →arrow data \u001b[31m\u001b[0mdata data →arrow γλυφ \u001b[K→arrow 汉字测试 γλυφ \u001b[Kalpha \u001b]0;title\u0007\u001b[31mdata →arrow data \u001b[1;32m\u001b[31m\u001b[0mdata data \u001b[38;5;9m汉字测试 汉字测试 \u001bZ\u001b[K\u001b[K\u001b[0m\u001b[0mγλυφ alpha \u001b[31malpha 汉字测试 alpha alpha \u001b]0;title\u0007\u001b[38;5;9malpha data data data beta \u001b[31m\u001b[1;32mdata →arrow \u001b[1;32m\u001b[K\u001b[mbeta →arrow \u001b[0m→arrow γλυφ beta data data γλυφ →arrow beta data 汉字测试 beta 汉字测试 \u001b[K→arrow →arrow alpha \u001b[mbeta data data \u001b[mγλυφ alpha →arrow 汉字测试 γλυφ γλυφ \u001b[31m\u001b[31m\u001b]0;title\u0007→arrow alpha \u001b[1;32mbeta γλυφ →arrow \u001b[K\u001b[38;5;9m\u001b[0m\u001b[1;32mγλυφ γλυφ \u001b[38;5;9m→arrow \u001b[31m\u001b[1;32m\u001b[1;32mγλυφ 汉字测试 alpha data \u001b]0;title\u0007\u001b[38;5;9malpha \u001b[38;5;9m→arrow 汉字测试 beta \u001b[0mbeta beta \u001b[38;5;9mγλυφ \u001b[mdata \u001b[mbeta \u001b[1;32mdata \u001b[mbeta γλυφ \u001b[m\u001b[31mbeta γλυφ 汉字测试 alpha data \u001b[K\u001b[0m汉字测试 beta alpha 汉字测试 \u001bZ\u001b[0m\u001b[38;5;9m\u001b[mγλυφ alpha γλυφ γλυφ 汉字测试 alpha alpha beta \u001b]0;title\u0007→arrow \u001b[38;5;9m汉字测试 \u001b[0m汉字测试 data \u001b[38;5;9m→arrow 汉字测试 \u001b[m\u001b[0mdata γλυφ γλυφ →arrow data data \u001b[31malpha →arrow data \u001b[K汉字测试 γλυφ \u001b[31m\u001b]0;title\u0007alpha \u001b[1;32m汉字测试 →arrow \u001bZdata \u001b[38;5;9mbeta →arrow 汉字测试 γλυφ \u001b[Kγλυφ beta alpha data γλυφ 汉字测试 beta beta data γλυφ →arrow alpha \u001b[31malpha \u001b[0m\u001b[38;5;9malpha \u001bZ汉字测试 \u001b[38;5;9malpha γλυφ γλυφ \u001bZ汉字测试 beta \u001b[38;5;9mbeta →arrow γλυφ \u001b[K→arrow data \u001b[Kγλυφ beta \u001b[1;32mγλυφ alpha γλυφ beta beta beta 汉字测试 \u001b[1;32mγλυφ \u001b[K汉字测试 beta →arrow γλυφ γλυφ 汉字测试 汉字测试 γλυφ 汉字测试 \u001b[31m汉字测试 \u001b[K\u001b[31mbeta \u001b[0malpha beta \u001b[38;5;9m\u001b[K\u001b[K汉字测试 alpha \u001b[1;32m→arrow 汉字测试 \u001b[38;5;9mdata 汉字测试 \u001b]0;title\u0007\u001b[mbeta \u001b[m\u001b[0mγλυφ γλυφ γλυφ →arrow \u001b[1;32m汉字测试 γλυφ beta \u001b]0;title\u0007\u001b[K→arrow beta 汉字测试 data 汉字测试 beta \u001bZ\u001b[31m\u001b[31m\u001b[Kγλυφ 汉字测试 data 汉字测试 beta beta \u001b[m\u001b]0;title\u0007\u001b[m\u001b[38;5;9malpha →arrow \u001b[m\u001b[m汉字测试 \u001b[1;32m\u001b[1;32m汉字测试 \u001b[mbeta \u001b[31mdata →arrow beta γλυφ beta \u001b[m→arrow \u001b[31m\u001b[mγλυφ \u001b[38;5;9mdata \u001b[m\u001b[0m\u001b]0;title\u0007\u001b[1;32m→arrow beta \u001b[m\u001b[m\u001b[K→arrow alpha \u001b]0;title\u0007\u001b[31mγλυφ γλυφ alpha data 汉字测试 beta \u001bZ\u001b[m\u001b[1;32mγλυφ 汉字测试 \u001bZ\u001b[K\u001bZalpha γλυφ →arrow 汉字测试 alpha γλυφ \u001bZ\u001b[m汉字测试 →arrow \u001b[0mbeta data \u001b[K汉字测试 alpha data beta \u001b[1;32m\u001b[1;32m\u001b[38;5;9m\u001b[1;32m\u001b[38;5;9m\u001b[38;5;9m汉字测试 \u001b[Kγλυφ \u001b[K→arrow γλυφ 汉字测试 →arrow \u001b[0m\u001bZ\u001b[38;5;9m\u001b[K\u001b[38;5;9mγλυφ →arrow γλυφ 汉字测试 beta \u001b[38;5;9m\u001b[31mbeta \u001b[31m\u001b]0;title\u0007汉字测试 →arrow beta 汉字测试 \u001b[31mdata 汉字测试 \u001b[38;5;9mbeta data γλυφ →arrow \u001bZdata \u001b[m→arrow \u001b[31m汉字测试 →arrow \u001bZ\u001b]0;title\u0007alpha \u001b[mγλυφ beta \u001b[m\u001b[1;32mγλυφ \u001b[1;32m\u001b[1;32m→arrow 汉字测试 beta 汉字测试 汉字测试 汉字测试 γλυφ \u001b[38;5;9mγλυφ \u001b[1;32m\u001b[m\u001b[0mbeta \u001b[0m\u001b[38;5;9mbeta γλυφ →arrow beta \u001b[mγλυφ \u001b[1;32m\u001b[malpha \u001b[38;5;9mdata 汉字测试 →arrow →arrow \u001b]0;title\u0007\u001b[31mγλυφ \u001b[mdata beta alpha \u001b[Kγλυφ data data data γλυφ 汉字测试 alpha \u001b]0;title\u0007\u001b[38;5;9m\u001b[38;5;9m\u001b[38;5;9m\u001b[31m\u001b[31m→arrow \u001b[K\u001b[Kdata \u001b[1;32m\u001b[0mdata →arrow data \u001b[1;32m\u001bZ\u001b[m\u001b[0m汉字测试 →arrow beta \u001b[0m汉字测试 alpha \u001b[38;5;9m\u001b]0;title\u0007\u001b[38;5;9mbeta \u001b[31m\u001b[1;32m→arrow \u001b]0;title\u0007beta beta \u001b[38;5;9m汉字测试 γλυφ \u001b[mγλυφ \u001b[31m→arrow \u001b[K\u001b[1;32m汉字测试 \u001b[K汉字测试 \u001b[0malpha \u001b[1;32m\u001b[0m→arrow 汉字测试 beta beta data γλυφ \u001b[Kγλυφ \u001b[0m\u001b[1;32malpha 汉字测试 \u001b[1;32m\u001b[K\u001b[38;5;9m\u001b[0mγλυφ alpha alpha \u001b[m\u001b[0m\u001bZγλυφ data 汉字测试 \u001b[38;5;9m→arrow data beta \u001b[31mdata γλυφ \u001b[38;5;9mbeta \u001bZ→arrow alpha →arrow \u001b[31m\u001b[0m\u001b[mγλυφ \u001b[m\u001b[Kbeta data →arrow \u001b[K\u001b[31mbeta γλυφ →arrow data \u001b[31m\u001b[31malpha \u001b[31m\u001b[0malpha →arrow 汉字测试 \u001b]0;title\u0007\u001b[malpha \u001b[31m\u001b[31mdata \u001b]0;title\u0007\u001b[0malpha \u001b[0m汉字测试 \u001b[31mγλυφ 汉字测试 \u001bZ\u001bZ\u001b[38;5;9mbeta →arrow 汉字测试 \u001b[38;5;9m\u001b[31m→arrow \u001b]0;title\u0007beta 汉字测试 \u001b]0;title\u0007\u001b]0;title\u0007\u001b]0;title\u0007→arrow 汉字测试 \u001b[31m\u001b[malpha data 汉字测试 data beta 汉字测试 →arrow γλυφ \u001b[0m\u001b[mγλυφ beta \u001b[38;5;9malpha \u001b[0m\u001b[mγλυφ \u001b[K汉字测试 \u001b[1;32m\u001b[31malpha alpha \u001b[m\u001bZγλυφ 汉字测试 γλυφ alpha \u001b[31mdata γλυφ \u001b[1;32mdata \u001b[malpha \u001b[Kγλυφ \u001b[31m→arrow beta \u001b[K\u001b[K\u001b[1;32malpha \u001b]0;title\u0007\u001b[31m\u001b[1;32mbeta \u001b[Kbeta γλυφ alpha γλυφ \u001b[38;5;9malpha \u001b[31mdata \u001b]0;title\u0007beta \u001b[K\u001b[m\u001b[K\u001b[K汉字测试 \u001b[K\u001b[38;5;9mdata alpha \u001b[Kdata \u001b[mdata γλυφ beta \u001b[38;5;9m\u001b[mγλυφ \u001b[mbeta \u001b[m\u001bZ\u001b[38;5;9m\u001b]0;title\u0007\u001b[K\u001b[malpha 汉字测试 \u001b[38;5;9mγλυφ beta →arrow beta →arrow γλυφ \u001b[31m→arrow \u001b[38;5;9mγλυφ alpha 汉字测试 beta \u001b[31malpha \u001b[Kalpha alpha \u001b[m\u001b[38;5;9m\u001b[1;32m\u001b[0m→arrow data \u001b]0;title\u0007汉字测试 \u001b[38;5;9mbeta \u001b[mbeta \u001b[0m汉字测试 alpha 汉字测试 \u001b[mdata γλυφ \u001bZ\u001b[1;32mbeta data alpha γλυφ data \u001b[malpha \u001b[38;5;9mbeta beta alpha \u001b[0m\u001b[31m\u001b[m\u001b[31m汉字测试 \u001b[K\u001b[38;5;9mdata \u001b[38;5;9malpha alpha \u001bZ\u001b]0;title\u0007beta \u001b[0m\u001bZ汉字测试 γλυφ γλυφ γλυφ \u001b[malpha \u001b]0;title\u0007汉字测试 汉字测试 \u001b[0m\u001b[Kbeta \u001b[mbeta beta γλυφ 汉字测试 \u001b[1;32m\u001b[Kdata →arrow \u001b[31m→arrow γλυφ γλυφ 汉字测试 \u001b]0;title\u0007\u001b[m\u001b[1;32malpha data \u001bZalpha \u001b]0;title\u0007\u001b[K→arrow \u001b[m\u001b[mγλυφ \u001b[m\u001b[31m\u001b[1;32m→arrow \u001b[31m\u001b[m→arrow 汉字测试 →arrow beta alpha γλυφ \u001b[Kbeta \u001bZ\u001b[38;5;9mdata γλυφ γλυφ alpha \u001b[31m\u001b[K\u001b[1;32m\u001b[m\u001b]0;title\u0007data →arrow alpha →arrow beta beta data beta 汉字测试 alpha \u001b[31malpha beta \u001b[0m\u001b[38;5;9m\u001b[31m\u001b[38;5;9m→arrow \u001b[K\u001b[Kalpha beta \u001b[31m\u001b[1;32mdata \u001b[1;32m\u001b[K\u001b[m\u001b]0;title\u0007\u001b[31m\u001b[38;5;9m\u001b[Kbeta →arrow γλυφ \u001bZalpha γλυφ →arrow →arrow \u001b[m\u001b[K\u001b[1;32m\u001b[malpha 汉字测试 \u001b[mbeta →arrow γλυφ alpha \u001b[0m\u001b[0malpha beta \u001b[m\u001b[1;32m\u001b[1;32malpha →arrow γλυφ \u001b[38;5;9m\u001b[0m\u001b[1;32m\u001bZ→arrow \u001b[38;5;9m\u001b[31malpha alpha 汉字测试 →arrow data beta data \u001b]0;title\u0007γλυφ \u001b[K\u001b[0m→arrow γλυφ alpha \u001b[31m\u001b[m→arrow \u001b]0;title\u0007\u001b[38;5;9m\u001b[0m\u001b[malpha 汉字测试 \u001b[31mγλυφ →arrow \u001b[Kalpha alpha beta \u001b[38;5;9mbeta →arrow data γλυφ beta beta \u001b[38;5;9malpha \u001b[38;5;9m\u001b[m汉字测试 \u001b[m\u001b]0;title\u0007alpha \u001b[K\u001b[38;5;9m\u001b[mγλυφ \u001b[Kγλυφ data 汉字测试 \u001b]0;title\u0007\u001b[0mγλυφ →arrow →arrow →arrow \u001b[1;32mγλυφ 汉字测试 汉字测试 \u001b[0m\u001b[m\u001b[38;5;9mdata 汉字测试 \u001b[1;32m\u001b[0m\u001b[31mγλυφ \u001b]0;title\u0007alpha \u001b[38;5;9m\u001b[31m\u001b[31m\u001b[31malpha 汉字测试 alpha \u001b[0m\u001bZbeta →arrow \u001b[31malpha alpha \u001b[0mdata beta alpha alpha data →arrow beta γλυφ \u001b[31m\u001b[K\u001b[1;32mγλυφ \u001b[1;32m\u001b[1;32m汉字测试 data 汉字测试 beta beta \u001b[m\u001b[Kγλυφ data \u001b[mγλυφ 汉字测试 γλυφ \u001b[m\u001b]0;title\u0007→arrow →arrow \u001b[0m\u001b[mbeta beta 汉字测试 →arrow γλυφ γλυφ \u001b[38;5;9m→arrow \u001b[1;32mγλυφ \u001b]0;title\u0007data \u001b[1;32mbeta \u001b[31m→arrow 汉字测试 alpha data →arrow data \u001b[0m汉字测试 \u001b[m\u001b]0;title\u0007beta alpha \u001b[38;5;9m汉字测试 \u001b[mγλυφ beta \u001b[1;32m\u001b[malpha 汉字测试 \u001b[1;32m\u001b[0m\u001b[31m\u001bZalpha →arrow alpha \u001b[1;32mdata beta data →arrow \u001b]0;title\u0007beta data γλυφ alpha \u001b[38;5;9malpha alpha data \u001bZ\u001b[38;5;9m汉字测试 alpha \u001bZ\u001b[38;5;9mdata alpha →arrow →arrow \u001b[31m\u001b[Kbeta alpha γλυφ beta \u001b[mbeta \u001b[31mγλυφ \u001bZ\u001bZ\u001b[K\u001bZbeta alpha \u001b[1;32m\u001b[31m\u001b]0;title\u0007beta \u001b]0;title\u0007\u001b[1;32malpha beta beta \u001b[0m\u001bZγλυφ alpha \u001b[mdata \u001b[38;5;9m汉字测试 beta \u001b[38;5;9malpha \u001b[Kdata \u001b[0m汉字测试 γλυφ →arrow data \u001b[31mdata γλυφ \u001b[1;32m汉字测试 alpha \u001b]0;title\u0007alpha \u001b[38;5;9m\u001bZalpha 汉字测试 alpha beta γλυφ \u001b[38;5;9m\u001b[Kdata \u001b[1;32malpha data \u001b]0;title\u0007\u001b[m\u001b[1;32m\u001b[K\u001b[38;5;9m\u001b[38;5;9mγλυφ \u001b[m→arrow →arrow \u001b[1;32mγλυφ \u001b[31mdata →arrow \u001b[malpha \u001b]0;title\u0007\u001b[K\u001b[0mbeta 汉字测试 \u001b[31m→arrow \u001b[K\u001b]0;title\u0007alpha beta \u001b]0;title\u0007\u001b[0m汉字测试 beta →arrow beta γλυφ →arrow beta \u001b[m\u001b[K\u001b]0;title\u0007\u001b[m\u001b[0m\u001b[1;32m\u001b[38;5;9mbeta γλυφ data alpha γλυφ \u001b[mγλυφ \u001b[1;32m汉字测试 →arrow 汉字测试 汉字测试 \u001b]0;title\u0007beta alpha \u001b[0mbeta data \u001b[0malpha \u001b[31m\u001b[38;5;9m→arrow data \u001b[31m\u001b]0;title\u0007\u001b[malpha γλυφ \u001b[31m\u001b[38;5;9m\u001bZ\u001b]0;title\u0007alpha \u001b[38;5;9m\u001b[mdata \u001b[31m→arrow alpha \u001b[Kbeta \u001bZ\u001b[0m\u001b[38;5;9m\u001b[m\u001b[1;32mγλυφ alpha data \u001b[31mbeta data \u001b[1;32mbeta →arrow \u001b[mbeta \u001b[31m\u001b[31m\u001b[m\u001b[1;32mdata γλυφ \u001b[38;5;9m→arrow 汉字测试 alpha alpha \u001b[K汉字测试 \u001b[m\u001b[m\u001b[38;5;9m\u001b[31m\u001b[m\u001b[K\u001b[mγλυφ data \u001b[31m\u001b[38;5;9m→arrow 汉字测试 \u001b[38;5;9mdata data alpha \u001b[38;5;9m汉字测试 \u001b[38;5;9m\u001b[31m\u001b[31mbeta \u001b[Kdata data data 汉字测试 γλυφ \u001b]0;title\u0007γλυφ \u001b[m\u001b[38;5;9m汉字测试 data 汉字测试 \u001b[Kγλυφ \u001b]0;title\u0007data \u001b[mdata γλυφ \u001b[1;32m\u001b[31mbeta 汉字测试 \u001b[38;5;9m→arrow \u001b[1;32m汉字测试 beta →arrow γλυφ beta γλυφ alpha \u001b[38;5;9m\u001bZbeta \u001b[38;5;9m\u001b[1;32m\u001b[K→arrow \u001b[38;5;9m\u001b[K\u001b[0mγλυφ alpha beta \u001b[1;32mbeta beta →arrow →arrow 汉字测试 γλυφ alpha \u001b[31mdata \u001b]0;title\u0007→arrow \u001b[38;5;9m\u001b[1;32m\u001b]0;title\u0007\u001b[0m→arrow beta \u001b[Kdata data data beta data data γλυφ γλυφ 汉字测试 \u001b[mγλυφ \u001bZ汉字测试 \u001b]0;title\u0007\u001b]0;title\u0007\u001b[0m\u001bZ\u001b[31mγλυφ data \u001b[38;5;9m\u001b[Kdata γλυφ γλυφ \u001b[0m\u001b[K\u001b[0m→arrow alpha \u001b[K\u001b]0;title\u0007data \u001b[38;5;9mdata →arrow \u001b[m\u001b[38;5;9m汉字测试 \u001bZbeta beta \u001b[Kalpha \u001b[38;5;9m\u001b[0m\u001bZ\u001b[31m\u001b[38;5;9m→arrow \u001b[0m→arrow →arrow →arrow →arrow \u001b[0m汉字测试 data \u001b[1;32m→arrow alpha →arrow \u001b[38;5;9m汉字测试 \u001b[38;5;9m\u001b[38;5;9mdata \u001b]0;title\u0007\u001b]0;title\u0007\u001b]0;title\u0007\u001b[38;5;9mγλυφ \u001b]0;title\u0007\u001b[1;32mγλυφ 汉字测试 \u001b[malpha \u001b[0m汉字测试 beta \u001bZ\u001b[K\u001b[K→arrow \u001b[38;5;9mγλυφ γλυφ \u001b[1;32mγλυφ →arrow \u001bZdata γλυφ \u001bZ\u001b[K\u001bZ\u001bZγλυφ \u001b]0;title\u0007\u001bZ\u001b[38;5;9m\u001b[1;32m\u001b[1;32m\u001b[m\u001b[mdata beta \u001b[K\u001bZ\u001b[38;5;9m\u001bZalpha \u001b[0m\u001b[K→arrow 汉字测试 beta beta alpha \u001b[m汉字测试 beta →arrow \u001b[38;5;9malpha data →arrow 汉字测试 γλυφ \u001b[38;5;9m\u001b[m\u001b]0;title\u0007\u001b]0;title\u0007\u001b[Kbeta beta \u001bZγλυφ \u001b[1;32m\u001b[31m\u001b[31m\u001b[38;5;9mγλυφ \u001bZdata \u001b[0m\u001b[31mγλυφ data \u001b[0m\u001b[31m→arrow \u001b[38;5;9malpha beta \u001bZdata \u001b[Kdata \u001b[1;32mdata 汉字测试 γλυφ \u001b[K\u001b]0;title\u0007→arrow 汉字测试 \u001b[38;5;9m\u001b[K\u001b[38;5;9mbeta \u001b[38;5;9m\u001b[31m→arrow →arrow \u001b[31m\u001b[31m\u001b]0;title\u0007→arrow 汉字测试 \u001b[malpha beta \u001b[38;5;9mγλυφ data γλυφ \u001b]0;title\u0007\u001b[m\u001bZalpha beta \u001b[0m\u001b[0m汉字测试 \u001b[mdata 汉字测试 \u001b[0m→arrow \u001b[0mγλυφ data data \u001b[0m\u001bZ\u001b[m\u001b[0mbeta γλυφ alpha \u001b[K汉字测试 \u001b[0m\u001bZ\u001b[38;5;9mγλυφ beta \u001b[Kalpha γλυφ 汉字测试 \u001b[31mγλυφ γλυφ beta beta \u001b[1;32m→arrow 汉字测试 \u001b[38;5;9m\u001b]0;title\u0007data \u001b[K\u001b[31mdata γλυφ \u001b]0;title\u0007alpha γλυφ \u001b[1;32m\u001b[Kalpha data data \u001b[1;32m\u001b[m→arrow 汉字测试 →arrow beta γλυφ \u001b[38;5;9m\u001bZalpha \u001b[Kalpha \u001bZ\u001b[m\u001b[0m\u001b[38;5;9m→arrow \u001b[m→arrow beta \u001b[Kbeta \u001b[38;5;9m\u001b[Kdata beta 汉字测试 \u001b[Kγλυφ \u001b[38;5;9m\u001b]0;title\u0007γλυφ \u001b[0m汉字测试 汉字测试 \u001b[31mdata \u001b[1;32m\u001b[m\u001b[Kdata \u001b[K\u001b]0;title\u0007\u001b[1;32mbeta \u001b[0mdata \u001b[31m汉字测试 γλυφ →arrow data γλυφ alpha →arrow \u001b[0m\u001b[38;5;9mbeta beta \u001b[malpha 汉字测试 data →arrow γλυφ \u001b[1;32mdata alpha beta \u001b[31m\u001b[Kbeta →arrow data \u001b[31m\u001b[38;5;9mdata \u001b[38;5;9malpha \u001b[Kγλυφ \u001b]0;title\u0007→arrow data \u001b[1;32m\u001b[31m\u001b[0m\u001b[1;32m\u001b[38;5;9m→arrow 汉字测试 beta γλυφ 汉字测试 →arrow \u001b[mdata alpha \u001b[mγλυφ →arrow \u001b[31mγλυφ beta \u001b[1;32mdata \u001b[0m→arrow beta \u001b[31m\u001b[31m\u001b[1;32m汉字测试 data data γλυφ ","expected":"汉字测试 data \u001b[0m\u001b[38;5;9m汉字测试 \u001b[mbeta \u001b[0mγλυφ \u001b[0malpha alpha \u001b[31m\u001b[38;5;9malpha \u001b[mdata \u001b[K…"},{"label":"sgr-mixed-10k-narrow","maxWidth":12,"text":"\u001b[31mdata 汉字测试 beta data \u001b[31mγλυφ \u001b[1;32m\u001b[0m\u001b[m\u001b[m\u001b[31m\u001b[1;32mdata \u001b[mγλυφ →arrow →arrow alpha \u001b[1;32m\u001bZ\u001b[31m汉字测试 \u001bZ→arrow data \u001b[K\u001b[K\u001b[31mbeta \u001b[38;5;9m\u001b[K\u001b[38;5;9mγλυφ γλυφ alpha γλυφ →arrow γλυφ →arrow data \u001b[0m\u001b[Kγλυφ →arrow data \u001bZdata beta \u001b[31m\u001b[mγλυφ \u001b[38;5;9mdata beta γλυφ γλυφ alpha γλυφ alpha \u001b]0;title\u0007\u001b[mdata →arrow data 汉字测试 \u001b[0m→arrow \u001b[0mγλυφ \u001b[1;32m汉字测试 \u001b[31m\u001b[31m\u001b[malpha \u001b[0mdata \u001b]0;title\u0007\u001b]0;title\u0007data γλυφ 汉字测试 beta data \u001b[1;32mdata 汉字测试 \u001b[Kbeta \u001b]0;title\u0007data \u001bZγλυφ 汉字测试 beta γλυφ →arrow 汉字测试 →arrow γλυφ \u001b[38;5;9m\u001b]0;title\u0007→arrow \u001b]0;title\u0007\u001b[31mdata beta \u001b[31m→arrow data beta beta 汉字测试 data data γλυφ \u001b[malpha 汉字测试 γλυφ →arrow \u001b[31m\u001b[0m\u001b]0;title\u0007→arrow →arrow \u001b[0mdata 汉字测试 →arrow alpha \u001b[38;5;9m\u001b[31mbeta beta data \u001b[0malpha \u001b[1;32mdata \u001b[0m\u001b[0mγλυφ \u001b[38;5;9m\u001b[Kdata \u001b[31m→arrow \u001b[mbeta \u001b[31mdata \u001b[0m\u001b[0m\u001bZalpha beta \u001b[1;32malpha \u001b[m汉字测试 data \u001b[0m\u001b[0m→arrow \u001b[0m\u001b]0;title\u0007\u001b[m\u001b[31m\u001b[1;32mdata data →arrow data beta \u001bZbeta \u001b[0mdata \u001bZbeta γλυφ 汉字测试 alpha γλυφ γλυφ \u001b[0mdata data \u001b[Kalpha 汉字测试 \u001b[0m\u001b[mγλυφ alpha beta \u001b[38;5;9mbeta \u001b[0mbeta →arrow \u001b[0mbeta \u001b[38;5;9m\u001b[0mdata data data data γλυφ data data →arrow γλυφ \u001b[1;32mbeta alpha γλυφ beta \u001b[1;32mγλυφ \u001b[K\u001b[1;32malpha \u001b[1;32malpha alpha γλυφ γλυφ \u001b]0;title\u0007beta \u001b]0;title\u0007data data \u001b]0;title\u0007beta \u001b[1;32m\u001b[38;5;9m\u001b[1;32m\u001b[1;32m→arrow data alpha \u001b[1;32m\u001bZalpha 汉字测试 \u001b[31m汉字测试 \u001b[K\u001b[0m→arrow data →arrow \u001b[Kdata →arrow data \u001b[K\u001b[K汉字测试 alpha γλυφ \u001b[0m→arrow \u001b[31m汉字测试 \u001b[31m\u001bZ\u001b[38;5;9mdata data →arrow γλυφ →arrow \u001b[Kdata 汉字测试 \u001b[0mdata \u001b[Kdata data data \u001b[38;5;9mbeta \u001bZdata 汉字测试 →arrow \u001b[38;5;9m\u001b[0mγλυφ \u001bZγλυφ alpha \u001b[m\u001b[m\u001b[K汉字测试 beta \u001b[0malpha alpha γλυφ \u001b[mdata \u001b[31mbeta \u001b[Kbeta \u001b[0m→arrow γλυφ 汉字测试 \u001b[1;32mγλυφ alpha \u001b[31mdata alpha →arrow →arrow 汉字测试 \u001b[1;32malpha \u001b]0;title\u0007data \u001bZdata γλυφ \u001b[31mdata \u001b]0;title\u0007γλυφ alpha \u001b[1;32mdata data beta \u001b]0;title\u0007\u001b[0m\u001b[31m\u001b[Kbeta →arrow alpha →arrow →arrow data data \u001b[K\u001b[0m\u001b[31malpha \u001b[0m\u001b[m汉字测试 beta \u001b[0m→arrow alpha →arrow alpha beta alpha \u001b[m\u001b[31m→arrow →arrow 汉字测试 \u001b[31m→arrow 汉字测试 data 汉字测试 \u001b[38;5;9m\u001b[31mγλυφ \u001b[1;32mγλυφ →arrow 汉字测试 汉字测试 汉字测试 \u001b[m\u001b[31m\u001b[1;32malpha 汉字测试 alpha \u001b[31mbeta \u001b[38;5;9mbeta 汉字测试 \u001b[m\u001b[K汉字测试 →arrow beta γλυφ →arrow γλυφ \u001b]0;title\u0007\u001b[mdata \u001b[0mbeta \u001b[K\u001bZ→arrow data \u001b]0;title\u0007alpha \u001b[38;5;9m\u001b[Kdata \u001b[38;5;9m\u001b[m汉字测试 data \u001b]0;title\u0007汉字测试 \u001b[1;32malpha \u001b[0m汉字测试 \u001b[38;5;9malpha alpha beta 汉字测试 汉字测试 beta γλυφ →arrow \u001b[31m\u001b[K\u001b[31m\u001b]0;title\u0007\u001b[0mγλυφ alpha beta data \u001b[31mdata data \u001bZγλυφ γλυφ γλυφ \u001b[m→arrow \u001b[38;5;9m汉字测试 汉字测试 γλυφ \u001b[K→arrow \u001b[0m\u001b[1;32mγλυφ data data data 汉字测试 \u001b[1;32mγλυφ →arrow beta γλυφ \u001b[0mbeta \u001b[0m\u001b[0malpha \u001b[38;5;9mbeta \u001bZalpha \u001b]0;title\u0007\u001b[38;5;9mγλυφ beta data \u001b[Kγλυφ \u001b[0m→arrow data \u001b[1;32mdata \u001b[31mbeta \u001b[0m\u001b[m\u001b[Kdata data \u001b[0m→arrow \u001b[K\u001b[Kalpha →arrow \u001b[Kbeta beta \u001b[31m\u001b[Kγλυφ beta data beta \u001b[Kdata \u001b[0mγλυφ →arrow →arrow alpha \u001b[mbeta alpha →arrow alpha \u001b[0mγλυφ 汉字测试 γλυφ data γλυφ \u001bZ\u001b[Kγλυφ \u001b[mbeta \u001b]0;title\u0007\u001b[0mbeta 汉字测试 汉字测试 →arrow \u001b[0m\u001b[K\u001b[m\u001b[38;5;9m\u001b[38;5;9m\u001b[m\u001bZ\u001b[1;32mbeta \u001b[31m\u001b[38;5;9m汉字测试 →arrow beta alpha \u001b[0m\u001b[1;32m\u001b[38;5;9m\u001b[38;5;9m\u001b[38;5;9m\u001b[Kγλυφ beta 汉字测试 \u001b[mdata alpha data \u001b[Kdata \u001b[K\u001b[31m→arrow beta γλυφ 汉字测试 \u001b[Kγλυφ data γλυφ \u001b[38;5;9m→arrow data alpha γλυφ data \u001b[31m\u001b[K\u001b[38;5;9mdata 汉字测试 alpha \u001b[31m\u001b[31mbeta \u001b[0mγλυφ \u001b[1;32m\u001b[38;5;9m\u001b[K\u001b[K\u001b[0m\u001bZ→arrow γλυφ 汉字测试 beta \u001bZalpha γλυφ beta \u001b[1;32malpha γλυφ \u001b[31m\u001b[31m\u001b[38;5;9m→arrow \u001b[31malpha 汉字测试 data \u001b[1;32mbeta 汉字测试 γλυφ alpha \u001b[38;5;9m汉字测试 data \u001b[0mbeta alpha →arrow beta beta γλυφ γλυφ \u001b[0m\u001b[K\u001b[0m\u001b[31m→arrow \u001b[38;5;9malpha \u001b[0m\u001b[K→arrow \u001b[0m\u001b]0;title\u0007\u001b[38;5;9m\u001b[0malpha \u001b[0m\u001b]0;title\u0007\u001b[38;5;9mbeta beta \u001b[K\u001b[1;32mbeta →arrow 汉字测试 \u001b[38;5;9m\u001b[K→arrow \u001b[0malpha beta \u001b[0m汉字测试 →arrow alpha 汉字测试 →arrow \u001b[mdata beta \u001b[38;5;9m汉字测试 beta \u001b[31m\u001b[38;5;9m\u001b[Kalpha data \u001b[31m汉字测试 beta →arrow \u001b[38;5;9malpha data alpha \u001b[1;32m\u001b[0m\u001bZγλυφ \u001b[Kγλυφ beta →arrow \u001b[38;5;9m\u001b[m\u001b]0;title\u0007\u001b[1;32mγλυφ alpha \u001b[1;32m→arrow \u001bZγλυφ \u001b[1;32m\u001bZγλυφ 汉字测试 →arrow beta \u001b[malpha →arrow \u001b[1;32malpha alpha \u001b[31malpha alpha \u001b[31m\u001b[Kγλυφ beta \u001b[m\u001b[31mγλυφ \u001b[0m→arrow γλυφ beta alpha \u001b[0mγλυφ alpha alpha \u001b[38;5;9mbeta data γλυφ beta \u001b]0;title\u0007\u001bZγλυφ \u001bZ\u001b[0m\u001b[1;32mγλυφ beta 汉字测试 \u001b[0malpha \u001b[31m\u001b[38;5;9m\u001b[m\u001b[mbeta \u001b[Kalpha →arrow \u001b[38;5;9m\u001bZdata γλυφ \u001b]0;title\u0007γλυφ \u001b[K→arrow \u001bZ\u001b[38;5;9mbeta γλυφ 汉字测试 alpha data γλυφ \u001b[38;5;9m\u001bZγλυφ \u001b]0;title\u0007alpha →arrow 汉字测试 \u001b[31m汉字测试 \u001b[1;32m\u001b[K\u001b[K\u001b[0mbeta 汉字测试 \u001b]0;title\u0007beta data \u001bZ\u001b[m\u001b[m\u001b[0m\u001bZ\u001b[1;32m汉字测试 γλυφ \u001b[38;5;9m\u001b[K→arrow data →arrow \u001b[1;32m\u001b[K\u001b[1;32m汉字测试 汉字测试 beta →arrow data alpha \u001b[m→arrow \u001b[31m\u001bZγλυφ \u001b[1;32mγλυφ \u001b[m\u001bZ\u001b[31m\u001b[0malpha 汉字测试 \u001b[38;5;9m汉字测试 \u001b[38;5;9mγλυφ alpha γλυφ \u001b[1;32m\u001b[38;5;9m\u001b[mdata \u001b[38;5;9mdata \u001b]0;title\u0007\u001b[38;5;9m\u001b[1;32mγλυφ \u001b[38;5;9m\u001b[31m\u001b[1;32mbeta \u001b[31mγλυφ \u001b[38;5;9m→arrow \u001b[31malpha \u001b[38;5;9m汉字测试 \u001b[mbeta beta \u001b[Kγλυφ \u001b[38;5;9m\u001b]0;title\u0007\u001b[0m\u001b[1;32m\u001b[1;32m\u001b[31mbeta \u001b]0;title\u0007data data beta 汉字测试 \u001b]0;title\u0007\u001b[38;5;9m\u001bZdata \u001b[31mγλυφ \u001b[m→arrow \u001b]0;title\u0007beta →arrow 汉字测试 \u001b[mdata \u001b[31malpha γλυφ \u001b[38;5;9m→arrow beta 汉字测试 →arrow γλυφ →arrow \u001b]0;title\u0007\u001b]0;title\u0007汉字测试 alpha \u001b[31m汉字测试 \u001b[1;32m\u001b[38;5;9m\u001b[m\u001b[0m汉字测试 \u001b[m\u001b[38;5;9m→arrow γλυφ \u001b[38;5;9mbeta →arrow \u001b[Kdata alpha 汉字测试 \u001b[1;32m汉字测试 \u001bZ\u001bZalpha \u001b[31m\u001b[mbeta \u001b[m\u001b[K\u001bZ\u001b[1;32mγλυφ \u001b[K→arrow \u001b[m\u001b[1;32mbeta alpha \u001b[0m\u001b[38;5;9m\u001b[1;32mbeta →arrow beta beta 汉字测试 \u001b[0m汉字测试 alpha \u001bZalpha data \u001b[1;32mdata \u001b[malpha alpha \u001b]0;title\u0007\u001b[0m\u001b[0malpha 汉字测试 alpha \u001b[1;32mγλυφ \u001b[0malpha beta \u001b]0;title\u0007\u001bZ\u001b[38;5;9mγλυφ beta γλυφ \u001b[Kalpha \u001b[31m\u001b[0malpha alpha beta \u001b[1;32mγλυφ beta \u001b[0m\u001b[K汉字测试 汉字测试 \u001b[31mbeta \u001b[1;32m汉字测试 alpha \u001b[0m→arrow \u001b[mdata γλυφ alpha alpha beta data \u001b[mγλυφ γλυφ \u001b[0mγλυφ \u001b[0malpha beta \u001b[31mbeta alpha \u001b[malpha data \u001bZ\u001b[1;32m\u001b]0;title\u0007\u001b[38;5;9mγλυφ 汉字测试 γλυφ beta \u001b[38;5;9m\u001b[0m→arrow 汉字测试 →arrow beta beta beta beta →arrow →arrow \u001b[1;32mdata \u001b[0m\u001b[1;32malpha \u001b[m\u001b[38;5;9m汉字测试 \u001b[K\u001bZ汉字测试 \u001b[1;32m→arrow beta alpha \u001b[0mγλυφ →arrow \u001b[mdata →arrow \u001b]0;title\u0007data beta \u001b[Kbeta →arrow alpha 汉字测试 \u001bZ\u001b[K\u001b[0mdata \u001b[31m\u001b[Kγλυφ →arrow γλυφ beta \u001b[38;5;9m→arrow \u001b[38;5;9mdata →arrow 汉字测试 \u001b[38;5;9m\u001b[1;32m汉字测试 \u001b[1;32m\u001b[38;5;9m\u001b[0mγλυφ \u001b[K\u001b[1;32malpha alpha \u001b[K→arrow \u001b[1;32m\u001b[Kalpha \u001b[K汉字测试 \u001b[1;32m\u001b[Kγλυφ \u001b[0m→arrow data →arrow 汉字测试 γλυφ alpha 汉字测试 beta beta data data \u001bZ\u001b[1;32m\u001b]0;title\u0007\u001bZ\u001b]0;title\u0007\u001b[0m\u001b[31mdata data γλυφ \u001b[31m→arrow \u001b[m\u001b[31mdata →arrow data alpha \u001b[38;5;9mdata data \u001b[Kγλυφ \u001b[1;32malpha \u001b[m→arrow data →arrow alpha \u001b[0m\u001b]0;title\u0007alpha alpha \u001b[38;5;9m\u001b[m\u001b[38;5;9mbeta \u001b[m\u001b[31malpha alpha \u001b[m\u001b[K\u001b[m→arrow \u001b[K\u001b[1;32mγλυφ →arrow →arrow alpha \u001b[m\u001b[38;5;9m\u001b[31m\u001b[m汉字测试 γλυφ beta \u001b[0mbeta data \u001b[K\u001b[K\u001b[m→arrow \u001b[1;32m\u001b[31mbeta \u001b[31malpha \u001b[Kalpha γλυφ 汉字测试 \u001b[K\u001b[1;32mbeta →arrow alpha data 汉字测试 \u001b]0;title\u0007beta γλυφ \u001b[K\u001b[0m\u001b[31malpha \u001b[Kbeta \u001b[1;32mbeta \u001b[malpha →arrow 汉字测试 \u001b[1;32m→arrow \u001bZ\u001b[K\u001b[1;32mbeta 汉字测试 \u001b[K汉字测试 γλυφ γλυφ γλυφ \u001b[31m\u001b[Kγλυφ alpha γλυφ \u001b[31mγλυφ \u001bZ\u001b]0;title\u0007\u001b[0malpha →arrow \u001b[1;32m\u001b[m汉字测试 \u001b[38;5;9mbeta \u001b[m\u001b[0m\u001b]0;title\u0007汉字测试 \u001b[38;5;9m→arrow \u001b]0;title\u0007alpha \u001b[m→arrow \u001b[38;5;9m\u001b[0m\u001b[0mbeta \u001b[0m\u001b]0;title\u0007data →arrow γλυφ beta \u001b[38;5;9malpha beta \u001b[mdata γλυφ alpha \u001b[38;5;9m\u001b[38;5;9malpha \u001b[1;32m汉字测试 γλυφ \u001b[1;32mdata →arrow alpha \u001b[K\u001b[malpha \u001b[0mγλυφ \u001bZ\u001bZ汉字测试 γλυφ →arrow \u001b[31m\u001b[1;32mdata \u001b[0mγλυφ beta \u001b]0;title\u0007汉字测试 data \u001b[38;5;9malpha →arrow beta \u001b[38;5;9mγλυφ alpha \u001b[1;32mγλυφ 汉字测试 汉字测试 \u001b[38;5;9m\u001b[1;32m汉字测试 \u001b[0m\u001b[0mγλυφ \u001b[0mbeta data alpha alpha 汉字测试 γλυφ \u001bZ\u001b[38;5;9mbeta γλυφ alpha γλυφ alpha \u001b[m汉字测试 →arrow \u001b[0m汉字测试 γλυφ \u001b]0;title\u0007beta data \u001b[0m\u001b[1;32m\u001b]0;title\u0007\u001b[38;5;9m\u001b]0;title\u0007\u001b[1;32mbeta \u001b[31m汉字测试 γλυφ \u001b[1;32m→arrow 汉字测试 γλυφ 汉字测试 \u001b[m→arrow 汉字测试 beta 汉字测试 →arrow 汉字测试 \u001b[38;5;9m\u001b]0;title\u0007\u001b[38;5;9mdata 汉字测试 \u001b[m\u001b[38;5;9mdata \u001b[Kdata 汉字测试 \u001b[38;5;9mbeta \u001b[0m\u001bZbeta \u001b[1;32malpha 汉字测试 \u001b[1;32mbeta data beta \u001b[1;32m\u001b[38;5;9m→arrow beta \u001b[31mdata beta \u001b[m\u001b[38;5;9mdata \u001b[38;5;9m\u001b[0mdata beta data beta →arrow \u001b[malpha \u001b[31m\u001b[m→arrow \u001b[K→arrow \u001b[31m\u001b[m\u001b]0;title\u0007汉字测试 data beta \u001b[0m汉字测试 data γλυφ alpha \u001b[K\u001b[K→arrow \u001bZ\u001b[0m→arrow →arrow \u001b[m\u001bZγλυφ data \u001b]0;title\u0007beta γλυφ γλυφ \u001b[malpha \u001b[K\u001b[38;5;9m汉字测试 data \u001b[mγλυφ \u001bZ\u001b[0m\u001b[31m\u001b]0;title\u0007data 汉字测试 alpha \u001bZ\u001b]0;title\u0007→arrow beta \u001b[38;5;9mγλυφ \u001b[K\u001b[1;32m\u001b[K\u001b]0;title\u0007beta \u001b[0m汉字测试 data 汉字测试 \u001b]0;title\u0007beta data data →arrow \u001b[mbeta \u001b[K汉字测试 data 汉字测试 汉字测试 beta \u001b[31m\u001bZ→arrow \u001b[1;32m\u001b[mdata \u001bZalpha \u001b[0m\u001bZ\u001b[K\u001b[1;32m→arrow \u001b[1;32m→arrow 汉字测试 \u001b]0;title\u0007\u001b[31mdata \u001b[K\u001bZ→arrow 汉字测试 →arrow 汉字测试 beta \u001b[38;5;9m→arrow \u001b[31mγλυφ →arrow \u001b[31m\u001b[1;32malpha \u001b[38;5;9m\u001b[K\u001b[Kdata \u001b[Kγλυφ γλυφ \u001b[31mγλυφ →arrow \u001b[31mγλυφ γλυφ γλυφ \u001b[1;32m汉字测试 \u001b]0;title\u0007beta \u001b[K\u001b[K\u001b[31m→arrow \u001b]0;title\u0007\u001b[K\u001b[1;32malpha \u001b[31m汉字测试 γλυφ γλυφ →arrow \u001b]0;title\u0007data alpha γλυφ \u001b[0m\u001b[0m\u001b[38;5;9m\u001b[0m\u001bZ\u001b[31m\u001b[31m\u001b[31mdata \u001b[0mdata \u001b[1;32mγλυφ \u001b]0;title\u0007γλυφ →arrow beta data \u001b[1;32malpha beta γλυφ 汉字测试 beta data \u001b[mγλυφ \u001bZbeta \u001b[m\u001b[31mdata \u001b[38;5;9malpha \u001bZ\u001b[31malpha \u001b[0m→arrow \u001b[38;5;9m汉字测试 汉字测试 \u001b[mγλυφ \u001b[31malpha beta data \u001b[Kdata →arrow γλυφ data γλυφ \u001b[m汉字测试 \u001b[31m→arrow data →arrow \u001b[31m\u001b[38;5;9m汉字测试 alpha \u001b[0m\u001b[31mγλυφ \u001b[K→arrow γλυφ 汉字测试 汉字测试 \u001b[m汉字测试 alpha \u001b[0m\u001b[1;32m\u001b[K汉字测试 \u001b[0m\u001b[Kalpha 汉字测试 \u001b[1;32mbeta beta \u001b[0malpha data \u001b[31mγλυφ γλυφ 汉字测试 \u001b]0;title\u0007\u001b[K→arrow 汉字测试 \u001b[31malpha \u001b[31mγλυφ \u001b[K\u001b[malpha \u001b[1;32mbeta 汉字测试 beta →arrow \u001b[m\u001b[m\u001b[mdata alpha 汉字测试 beta \u001b[1;32mdata \u001b[malpha \u001b[0mbeta \u001b[mγλυφ γλυφ →arrow →arrow γλυφ beta data \u001b[31m\u001b[38;5;9m汉字测试 \u001b]0;title\u0007alpha \u001b]0;title\u0007alpha \u001b[0m\u001b[mγλυφ 汉字测试 beta γλυφ γλυφ data beta \u001b[31m\u001b[1;32m\u001b[1;32m汉字测试 γλυφ \u001b[38;5;9m\u001b[m\u001b]0;title\u0007data \u001b[31mγλυφ →arrow alpha \u001b[38;5;9mbeta 汉字测试 \u001b]0;title\u0007\u001b[38;5;9m→arrow \u001b]0;title\u0007\u001b[31m汉字测试 beta \u001b[38;5;9m\u001b[K\u001b[0m\u001b[38;5;9m\u001b[0mbeta beta data γλυφ →arrow \u001b[1;32m\u001b[Kbeta \u001b]0;title\u0007beta 汉字测试 beta alpha →arrow γλυφ \u001b[m\u001bZ\u001b[31malpha \u001b[31mbeta \u001b[m\u001b[0mγλυφ \u001b[0m\u001b[Kdata data \u001b[Kbeta \u001b[K\u001b[0m汉字测试 汉字测试 \u001b]0;title\u0007\u001b[38;5;9m\u001b[1;32m→arrow alpha alpha \u001b[1;32m\u001b[0m→arrow \u001b[K\u001b[1;32malpha alpha \u001b[m\u001b[38;5;9mbeta beta 汉字测试 汉字测试 \u001b[31malpha \u001b[1;32m\u001bZ\u001b[K→arrow \u001b[1;32m汉字测试 →arrow →arrow \u001b[38;5;9m\u001b[Kγλυφ data data alpha γλυφ →arrow data 汉字测试 汉字测试 \u001b[31mγλυφ \u001b[38;5;9malpha →arrow \u001b[mγλυφ \u001b[38;5;9mbeta \u001b[38;5;9m→arrow \u001b[mdata alpha γλυφ \u001b[m汉字测试 \u001b[38;5;9m\u001b[31m汉字测试 \u001b[0m\u001b[38;5;9mγλυφ \u001b[K汉字测试 \u001b[38;5;9mbeta γλυφ γλυφ alpha \u001bZ汉字测试 γλυφ beta 汉字测试 data \u001b[1;32m","expected":"\u001b[31mdata 汉字测\u001b[31m…\u001b[0m"},{"label":"osc-title","maxWidth":30,"text":"\u001b]0;window title\u0007plain text after osc that is long enough to truncate here","expected":"\u001b]0;window title\u0007plain text a…"},{"label":"bare-esc","maxWidth":25,"text":"before \u001bZ after bare esc char continues to exceed the width limit easily","expected":"before \u001bZ after bare esc…"},{"label":"csi-k-mixed-sgr","maxWidth":20,"text":"\u001b[31mred\u001b[K\u001b[32mgreen tail that keeps going past the boundary here ok","expected":"\u001b[31mred\u001b[K\u001b[32mgreen tail th\u001b[31m\u001b[32m…\u001b[0m"},{"label":"osc-inside-truncation","maxWidth":24,"text":"AAAAAAAAAAAAAAAAAAAA\u001b]0;t\u0007BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","expected":"AAAAAAAAAAAAAAAAAAAA\u001b]0…"},{"label":"esc-at-end","maxWidth":22,"text":"text almost at width limit\u001b","expected":"text almost at width …"},{"label":"sgr-exact-fit","maxWidth":31,"text":"\u001b[31mxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u001b[0m","expected":"\u001b[31mxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u001b[0m"},{"label":"emoji-mix","maxWidth":17,"text":"😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅😀🚀✅\u001b[1mbold\u001b[0myyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy","expected":"😀🚀✅😀🚀✅😀🚀✅😀🚀✅…"}]
@@ -25,7 +25,7 @@
25
25
  // mock 策略:与 run-spawn-integration.test.ts 一致(共享 helpers/spawn-mock.ts),
26
26
  // 额外注入 ctx.uiRequestHandler + ctx.dialogQueue + channel registry。
27
27
 
28
- import { execFileSync, spawn } from "node:child_process";
28
+ import { spawn } from "node:child_process";
29
29
  import * as fs from "node:fs";
30
30
 
31
31
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -36,7 +36,15 @@ vi.mock("node:child_process", async () => {
36
36
  const { FakeChild } = await import("./helpers/spawn-mock.ts");
37
37
  return {
38
38
  spawn: vi.fn(() => new FakeChild()),
39
- execFileSync: vi.fn(() => ""),
39
+ // buildEnvBlock git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
40
+ execFile: vi.fn(
41
+ (
42
+ _cmd: string,
43
+ _args: readonly string[],
44
+ _opts: unknown,
45
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
46
+ ) => cb(new Error("execFile not configured in this test")),
47
+ ),
40
48
  };
41
49
  });
42
50
 
@@ -88,7 +96,6 @@ import {
88
96
  } from "./helpers/spawn-mock.ts";
89
97
 
90
98
  const mockSpawn = vi.mocked(spawn);
91
- const mockExec = vi.mocked(execFileSync);
92
99
  const mockExistsSync = vi.mocked(fs.existsSync);
93
100
 
94
101
  const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
@@ -172,7 +179,6 @@ function makeAskUserCtx(
172
179
 
173
180
  beforeEach(() => {
174
181
  vi.clearAllMocks();
175
- mockExec.mockReturnValue("");
176
182
  mockExistsSync.mockReturnValue(false);
177
183
  // 静默 stdin-writer / factory 的 warn(序列化失败降级等场景)
178
184
  vi.spyOn(console, "warn").mockImplementation(() => {});
@@ -0,0 +1,132 @@
1
+ // src/execution/__tests__/before-agent-start-injection.test.ts
2
+ //
3
+ // SP-3: before_agent_start subagent 状态注入测试。
4
+ //
5
+ // 验证:
6
+ // TC-1: 有活跃 subagent 时 hook 返回 message(customType=subagent-status)
7
+ // TC-2: 无活跃 subagent 时 hook 返回 undefined
8
+ // TC-3: 超过 10 条截断显示 '+N more'
9
+ // TC-4: 快照内容格式正确(含 id、slug、status、rounds)
10
+
11
+ import { describe, expect, it, vi } from "vitest";
12
+
13
+ import { formatSubagentStatusSnapshot } from "../../index.ts";
14
+ import type { SubagentRecord } from "../types.ts";
15
+
16
+ // ── SubagentRecord stub 工厂 ──
17
+
18
+ function makeRecord(over: Partial<SubagentRecord> = {}): SubagentRecord {
19
+ return {
20
+ id: "sa-abc123",
21
+ agent: "general-purpose",
22
+ task: "test task",
23
+ slug: "test-slug",
24
+ status: "running",
25
+ mode: "background",
26
+ startedAt: Date.now(),
27
+ rootSessionId: "session-main",
28
+ parentRecordId: undefined,
29
+ depth: 0,
30
+ endedAt: undefined,
31
+ turns: 0,
32
+ totalTokens: 0,
33
+ model: "test/model",
34
+ thinkingLevel: undefined,
35
+ eventLog: [],
36
+ displayItems: [],
37
+ result: undefined,
38
+ error: undefined,
39
+ sessionFile: undefined,
40
+ round: 0,
41
+ ...over,
42
+ };
43
+ }
44
+
45
+ // ============================================================
46
+ // formatSubagentStatusSnapshot(纯函数测试)
47
+ // ============================================================
48
+
49
+ describe("formatSubagentStatusSnapshot", () => {
50
+ // TC-1: 有活跃 subagent 时返回非空快照
51
+ it("TC-1: 有活跃 subagent 时返回含 customType 消息内容", () => {
52
+ const records = [makeRecord({ id: "sa-001", slug: "fix-login", status: "running" })];
53
+ const result = formatSubagentStatusSnapshot(records);
54
+ expect(result).toContain("[subagent-status] 1 active subagent:");
55
+ expect(result).toContain("sa-001");
56
+ expect(result).toContain("fix-login");
57
+ expect(result).toContain("running");
58
+ });
59
+
60
+ // TC-2: 无活跃 subagent 时返回空列表 → 调用方不调用本函数(返回 undefined)
61
+ // 本测试验证空数组格式化结果的正确性(虽然 hook 层不会传入空数组)
62
+ it("TC-2: 空数组格式化为 0 active subagents", () => {
63
+ const result = formatSubagentStatusSnapshot([]);
64
+ expect(result).toBe("[subagent-status] 0 active subagents:");
65
+ });
66
+
67
+ // TC-3: 超过 10 条截断显示 '+N more'
68
+ it("TC-3: 超过 10 条截断显示 +N more", () => {
69
+ const records = Array.from({ length: 13 }, (_, i) =>
70
+ makeRecord({ id: `sa-${String(i).padStart(3, "0")}`, slug: `task-${i}`, status: "running" }),
71
+ );
72
+ const result = formatSubagentStatusSnapshot(records);
73
+ // 应显示前 10 条
74
+ expect(result).toContain("sa-000");
75
+ expect(result).toContain("sa-009");
76
+ // 不应显示第 11 条
77
+ expect(result).not.toContain("sa-010");
78
+ // 截断提示
79
+ expect(result).toContain("+3 more, use action:'list'");
80
+ });
81
+
82
+ // TC-4: 快照内容格式正确(含 id、slug、status、rounds)
83
+ it("TC-4: 快照格式正确——id/slug/status/rounds", () => {
84
+ const records = [
85
+ makeRecord({ id: "sa-aaa", slug: "deploy-app", status: "running", round: 0 }),
86
+ makeRecord({ id: "sa-bbb", slug: "run-tests", status: "idle", round: 3 }),
87
+ ];
88
+ const result = formatSubagentStatusSnapshot(records);
89
+ const lines = result.split("\n");
90
+
91
+ // 第一行:标题
92
+ expect(lines[0]).toBe("[subagent-status] 2 active subagents:");
93
+
94
+ // running record:round 0 不显示 rounds
95
+ expect(lines[1]).toBe("- sa-aaa (deploy-app): running");
96
+
97
+ // idle record:round 3 显示 rounds
98
+ expect(lines[2]).toBe("- sa-bbb (run-tests): idle, rounds 3");
99
+ });
100
+
101
+ // 补充:slug 为空时 fallback 到 agent 名
102
+ it("slug 为空时 fallback 到 agent 名", () => {
103
+ const records = [makeRecord({ id: "sa-x", slug: "", agent: "coder" })];
104
+ const result = formatSubagentStatusSnapshot(records);
105
+ expect(result).toContain("(coder)");
106
+ expect(result).not.toContain("()");
107
+ });
108
+ });
109
+
110
+ // ============================================================
111
+ // hook 逻辑集成测试(mock getSubagentService)
112
+ // ============================================================
113
+
114
+ describe("before_agent_start hook logic", () => {
115
+ // 动态导入以拿到真实 hook 注册后的状态。
116
+ // 由于 hook 注册在 index.ts 默认导出函数内,直接测 hook 回调逻辑
117
+ // 需要 mock getSubagentService。这里测 formatSubagentStatusSnapshot 的
118
+ // 集成行为(records → message 结构)来覆盖 hook 逻辑。
119
+
120
+ it("hook 消息结构符合 BeforeAgentStartEventResult.message 签名", () => {
121
+ const records = [makeRecord({ status: "running" })];
122
+ const content = formatSubagentStatusSnapshot(records);
123
+
124
+ // 模拟 hook 返回值结构
125
+ const result = { message: { customType: "subagent-status", content, display: true } };
126
+
127
+ expect(result.message.customType).toBe("subagent-status");
128
+ expect(result.message.display).toBe(true);
129
+ expect(result.message.content).toContain("[subagent-status]");
130
+ expect(result.message.content).toContain("sa-abc123");
131
+ });
132
+ });