@ghyper9023/pi-dev-workflow 0.4.1 → 0.4.3

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 (46) hide show
  1. package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +58 -0
  2. package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +13 -0
  3. package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +13 -0
  4. package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +13 -0
  5. package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +13 -0
  6. package/.pi-dev-output/pi-grill/answers/answer-mpfttme1-20260522-0230.md +13 -0
  7. package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +94 -0
  8. package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +215 -0
  9. package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +240 -0
  10. package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +253 -0
  11. package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +137 -0
  12. package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +258 -0
  13. package/.pi-dev-output/pi-plans/20260522-113000-grill-left-arrow-fix.md +274 -0
  14. package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +196 -0
  15. package/.pi-dev-output/pi-review/md/review-20260520-100000.md +91 -0
  16. package/.pi-dev-output/pi-review/md/review-20260521-140000.md +191 -0
  17. package/.pi-dev-output/pi-review/md/review-20260521-190000.md +189 -0
  18. package/.pi-dev-output/pi-review/md/review-20260521-204500.md +241 -0
  19. package/.pi-dev-output/pi-review/md/review-20260521-214500.md +270 -0
  20. package/.pi-dev-output/pi-review/md/review-20260521-215158.md +214 -0
  21. package/.pi-dev-output/pi-review/md/review-20260521-234500.md +201 -0
  22. package/.pi-dev-output/pi-review/md/review-20260521-235500.md +422 -0
  23. package/.pi-dev-output/pi-review/md/review-20260522-000000.md +212 -0
  24. package/.pi-dev-output/pi-review/md/review-20260522-003000.md +377 -0
  25. package/.pi-dev-output/pi-review/md/review-20260522-003500.md +296 -0
  26. package/.pi-dev-output/pi-review/md/review-20260522-105000.md +166 -0
  27. package/.pi-dev-output/pi-workflow/checkpoint-20260521-113000-fix-loopcount-timeout.json +402 -0
  28. package/.pi-dev-output/pi-workflow/checkpoint-20260521-1730-grill-input-wrap-back-fix.json +447 -0
  29. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230000-fix-timeout-display-loopcount-gitdiff.json +708 -0
  30. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230500-esc-double-press-confirm-workflow.json +365 -0
  31. package/.pi-dev-output/pi-workflow/checkpoint-20260521-235000-fix-gitdiff-loopcount.json +395 -0
  32. package/.pi-dev-output/pi-workflow/checkpoint-20260522-113000-grill-left-arrow-fix.json +473 -0
  33. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +30 -0
  34. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +49 -0
  35. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +59 -0
  36. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi5r22.json +76 -0
  37. package/.version/RELEASE-v0.4.2.md +31 -0
  38. package/.version/RELEASE-v0.4.3.md +42 -0
  39. package/README.md +21 -3
  40. package/extensions/dev-prompts.ts +16 -8
  41. package/extensions/grill-me-agent.ts +74 -8
  42. package/extensions/ui-helpers.ts +59 -7
  43. package/extensions/workflow-engine.ts +80 -32
  44. package/package.json +1 -1
  45. package/tests/test-loopcount-timeout-fix.mjs +336 -0
  46. package/themes/oh-my-pi-titanium.json +90 -0
@@ -0,0 +1,196 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Code Review Report</title>
7
+ <style>
8
+ *{margin:0;padding:0;box-sizing:border-box}
9
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f5f5f5;color:#333;line-height:1.6;padding:20px}
10
+ .container{max-width:960px;margin:0 auto}
11
+ h1{font-size:24px;margin-bottom:8px;color:#1a1a1a}
12
+ .summary{background:#fff;border-radius:8px;padding:20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.1)}
13
+ .summary-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}
14
+ .summary-item{background:#fafafa;padding:12px;border-radius:6px}
15
+ .summary-item .label{font-size:12px;color:#666;text-transform:uppercase}
16
+ .summary-item .value{font-size:20px;font-weight:700;margin-top:4px}
17
+ .bug-count .value{color:#d32f2f}
18
+ .warn-count .value{color:#f57c00}
19
+ .info-count .value{color:#1976d2}
20
+ .score{font-size:36px;font-weight:700;text-align:center;padding:16px;background:#fafafa;border-radius:6px;margin-top:12px}
21
+ .score.good{color:#2e7d32}
22
+ .score.fair{color:#f57c00}
23
+ .commit-msg{background:#e8f5e9;padding:12px 16px;border-radius:6px;margin-top:12px;font-family:"SF Mono","Consolas",monospace;font-size:13px;color:#2e7d32}
24
+ .file-group{background:#fff;border-radius:8px;margin-bottom:12px;box-shadow:0 1px 3px rgba(0,0,0,.1);overflow:hidden}
25
+ .file-header{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;cursor:pointer;transition:background .15s}
26
+ .file-header:hover{background:#f0f0f0}
27
+ .file-header .path{font-family:"SF Mono","Consolas",monospace;font-size:14px;font-weight:600;color:#1a1a1a}
28
+ .file-header .status{display:flex;gap:8px;align-items:center}
29
+ .badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
30
+ .badge.bug{background:#ffebee;color:#c62828}
31
+ .badge.warn{background:#fff3e0;color:#e65100}
32
+ .badge.info{background:#e3f2fd;color:#1565c0}
33
+ .file-body{display:none;padding:0 20px 20px;border-top:1px solid #eee}
34
+ .file-body.open{display:block}
35
+ .issue{margin:12px 0;padding:12px 16px;border-left:4px solid #ccc;background:#fafafa;border-radius:0 6px 6px 0}
36
+ .issue.bug{border-color:#d32f2f}
37
+ .issue.warn{border-color:#f57c00}
38
+ .issue.info{border-color:#1976d2}
39
+ .issue .tag{font-size:11px;font-weight:700;text-transform:uppercase;margin-bottom:4px}
40
+ .issue.bug .tag{color:#d32f2f}
41
+ .issue.warn .tag{color:#f57c00}
42
+ .issue.info .tag{color:#1976d2}
43
+ .issue .desc{font-size:14px;margin:4px 0}
44
+ .issue .code{background:#272822;color:#f8f8f2;padding:8px 12px;border-radius:4px;font-size:13px;font-family:"SF Mono","Consolas",monospace;margin:6px 0;overflow-x:auto;white-space:pre}
45
+ .diff-stats{font-size:12px;color:#666;margin-left:12px}
46
+ .add{color:#2e7d32}
47
+ .del{color:#c62828}
48
+ .toggle-icon{color:#999;font-size:12px}
49
+ </style>
50
+ </head>
51
+ <body>
52
+ <div class="container">
53
+ <h1>🔍 Code Review Report</h1>
54
+
55
+ <div class="summary">
56
+ <p><strong>审查范围:</strong>git diff HEAD(4 files changed, +56, -30)</p>
57
+
58
+ <div class="summary-grid">
59
+ <div class="summary-item bug-count"><div class="label">BUG</div><div class="value">2</div></div>
60
+ <div class="summary-item warn-count"><div class="label">敏感信息</div><div class="value">1</div></div>
61
+ <div class="summary-item info-count"><div class="label">可维护性</div><div class="value">1</div></div>
62
+ <div class="summary-item info-count"><div class="label">规范</div><div class="value">1</div></div>
63
+ </div>
64
+
65
+ <div class="score fair">评分:55/100</div>
66
+ <p style="margin-top:8px;font-size:14px;color:#666;text-align:center">存在 2 个 BUG(运行时错误),1 个敏感信息变更,扣分严重</p>
67
+
68
+ <div class="commit-msg">
69
+ <strong>💡 commit message 建议:</strong><br>
70
+ <code>feat: 增加 reviewTimeoutMs 独立超时配置,修复 workflow sub-step 状态重置函数名错误</code>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- workflow-engine.ts -->
75
+ <div class="file-group">
76
+ <div class="file-header" onclick="this.nextElementSibling.classList.toggle('open')">
77
+ <span>
78
+ <span class="path">extensions/workflow-engine.ts</span>
79
+ <span class="diff-stats"><span class="add">+27</span> <span class="del">-11</span></span>
80
+ </span>
81
+ <span class="status">
82
+ <span class="badge bug">2 BUG</span>
83
+ <span class="badge warn">1 敏感</span>
84
+ <span class="toggle-icon">▶</span>
85
+ </span>
86
+ </div>
87
+ <div class="file-body">
88
+ <div class="issue bug">
89
+ <div class="tag">🛑 BUG · 最高优先级</div>
90
+ <div class="desc"><strong>调用已重命名的函数导致 ReferenceError</strong></div>
91
+ <div class="desc">第 1193-1194 行调用 <code>setWidgetSubStepStatus()</code>,但该函数在第 783 行被重命名为 <code>resetWidgetSubStep()</code>(参数从 (status) 变为固定 "pending" 并增加重置 detail/tools/outputs 逻辑)。旧函数名已被删除,运行时将抛出 <code>ReferenceError: setWidgetSubStepStatus is not defined</code>。</div>
92
+ <div class="code">// 第 1193-1194 行(executeLoopGroup)
93
+ setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
94
+ setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");</div>
95
+ <div class="desc"><strong>修复:</strong>改为调用 <code>resetWidgetSubStep(stepIndex, step.loopAgentName!)</code> 等。</div>
96
+ </div>
97
+
98
+ <div class="issue bug">
99
+ <div class="tag">🛑 BUG · 最高优先级</div>
100
+ <div class="desc"><strong>runAgentWithProgress 中仍调用已删除的函数</strong></div>
101
+ <div class="desc">第 1114 行仍调用 <code>setWidgetSubStepStatus(stepIndex, agentName, subStatus)</code>,该函数已被删除。执行单步 agent 完成后也会抛出 ReferenceError。</div>
102
+ <div class="code">// 第 1114 行(runAgentWithProgress 末尾)
103
+ setWidgetSubStepStatus(stepIndex, agentName, subStatus);</div>
104
+ <div class="desc"><strong>修复:</strong>将旧函数逻辑恢复,或改写为直接修改 sub-step 状态。</div>
105
+ </div>
106
+
107
+ <div class="issue warn">
108
+ <div class="tag">⚠️ 敏感信息 · 高优先级</div>
109
+ <div class="desc"><strong>runAgentWithProgress 中 sub-step detail 硬编码中文提醒信息</strong></div>
110
+ <div class="desc">新增的 <code>detail: "超时时间${formatTimeout(timeoutMs)}"</code> 是 UI 展示文本,在 engine 层硬编码中文。当工具函数输出被序列化或用于日志管道时,中文文本可能引起编码问题或混乱。建议使用英文 key(如 <code>timeoutText</code> 或分离 data/ui 层)。</div>
111
+ </div>
112
+
113
+ <div class="issue info">
114
+ <div class="tag">🔧 可维护性 · 中优先级</div>
115
+ <div class="desc"><strong>loop-group 未展示 timeoutMs 但 reviewer 仍有独立超时</strong></div>
116
+ <div class="desc"><code>executeWorkflowBackground</code> 中 loop-group 步骤的 <code>updateWidgetStep</code> 调用传入 <code>timeoutMs: undefined</code>,这合理(loop-group 的 timeout 逻辑在内部)。但 reviewTimeoutMs 并未在 widget 中展示,建议在 sub-step detail 中体现 reviewer 的独立超时。</div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- dev-prompts.ts -->
122
+ <div class="file-group">
123
+ <div class="file-header" onclick="this.nextElementSibling.classList.toggle('open')">
124
+ <span>
125
+ <span class="path">extensions/dev-prompts.ts</span>
126
+ <span class="diff-stats"><span class="add">+16</span> <span class="del">-8</span></span>
127
+ </span>
128
+ <span class="status">
129
+ <span class="badge info">1 规范</span>
130
+ <span class="toggle-icon">▶</span>
131
+ </span>
132
+ </div>
133
+ <div class="file-body">
134
+ <div class="issue info">
135
+ <div class="tag">📐 规范 · 低优先级</div>
136
+ <div class="desc"><strong>所有 loop-group 配置的高 timeoutMs 应与 reviewTimeoutMs 有明确注释</strong></div>
137
+ <div class="desc">将 <code>timeoutMs</code> 从 900_000(15min)提升到 1_800_000(30min),并新增 <code>reviewTimeoutMs: 900_000</code>。改动意图明确,配置值合理。但所有步骤的 <code>timeoutMs</code> 和 <code>reviewTimeoutMs</code> 高度重复,建议提取为常量以提高可维护性。</div>
138
+ <div class="code">// 建议提取为命名常量
139
+ const LOOP_TIMEOUT_MS = 1_800_000; // 30min
140
+ const REVIEW_TIMEOUT_MS = 900_000; // 15min
141
+ const TRIM_TIMEOUT_MS = 1_200_000; // 20min</div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- ui-helpers.ts -->
147
+ <div class="file-group">
148
+ <div class="file-header" onclick="this.nextElementSibling.classList.toggle('open')">
149
+ <span>
150
+ <span class="path">extensions/ui-helpers.ts</span>
151
+ <span class="diff-stats"><span class="add">+1</span> <span class="del">-4</span></span>
152
+ </span>
153
+ <span class="status">
154
+ <span class="badge info">1 可维护性</span>
155
+ <span class="toggle-icon">▶</span>
156
+ </span>
157
+ </div>
158
+ <div class="file-body">
159
+ <div class="issue info">
160
+ <div class="tag">🔧 可维护性 · 中优先级</div>
161
+ <div class="desc"><strong>formatTimeout 改为 export,移除 running 状态下强制显示「第 1 次循环」的特判</strong></div>
162
+ <div class="desc"><code>formatTimeout</code> 被导出供 workflow-engine 使用,改动合理。移除 isRunning 分支的「第 1 次循环」硬编码,改为由 engine 层通过 <code>updateWidgetStep</code> 及时更新 <code>loopCount</code>,设计更清晰。pending 状态仍显示「第 0 次循环」,语义明确。</div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- .gitignore -->
168
+ <div class="file-group">
169
+ <div class="file-header" onclick="this.nextElementSibling.classList.toggle('open')">
170
+ <span>
171
+ <span class="path">.gitignore</span>
172
+ <span class="diff-stats"><span class="del">-4</span></span>
173
+ </span>
174
+ <span class="status">
175
+ <span class="badge warn">1 敏感</span>
176
+ <span class="toggle-icon">▶</span>
177
+ </span>
178
+ </div>
179
+ <div class="file-body">
180
+ <div class="issue warn">
181
+ <div class="tag">⚠️ 敏感信息 · 高优先级</div>
182
+ <div class="desc"><strong>移除 pi-review/ 和 pi-dev-output/ 的 gitignore 规则</strong></div>
183
+ <div class="desc">提交移除了 <code>pi-review/</code> 和 <code>pi-dev-output/</code> 的 ignore 规则。这两个目录是 pi 框架的产物输出目录,包含审查报告、构建产物等。如果属于不应提交到版本控制的生成产物,应保留 ignore 规则。请确认这是有意的行为(例如要将产物纳入版本管理),或是误删。</div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ </div>
189
+ <script>
190
+ document.querySelectorAll('.file-body.open').forEach(el=>el.classList.remove('open'));
191
+ document.querySelectorAll('.file-header').forEach(h=>{
192
+ h.addEventListener('click',function(){const b=this.querySelector('.toggle-icon');b.textContent=b.textContent==='▶'?'▼':'▶';});
193
+ });
194
+ </script>
195
+ </body>
196
+ </html>
@@ -0,0 +1,91 @@
1
+ # 代码审查报告 — workflow-engine 双 Bug 修复
2
+
3
+ **审查时间**: 2026-05-20
4
+ **审查范围**: `extensions/workflow-engine.ts` + `tests/test-workflow-engine-bugs.mjs`
5
+ **审查背景**: 修复 executeLoopGroup 缺少 exitCode 检查 (Bug A) 和 setTimeout cleanupWidget 竞态条件 (Bug B)
6
+
7
+ ---
8
+
9
+ ## 严重等级汇总
10
+
11
+ | 等级 | 数量 |
12
+ |------|------|
13
+ | 🔴 critical | 0 |
14
+ | 🟡 medium | 2 |
15
+ | 🟢 low | 2 |
16
+
17
+ ---
18
+
19
+ ## 🟡 中等问题
20
+
21
+ ### M1. `tests/test-workflow-engine.mjs` 中 deleteCheckpointFile 导出测试失败(pre-existing)
22
+
23
+ **文件**: `tests/test-workflow-engine.mjs` 第 222 行
24
+ **问题**: 测试断言 `source.includes("export function deleteCheckpointFile")` 但 `workflow-engine.ts` 中并没有 `deleteCheckpointFile` 函数。这不是本次修复引入的问题,而是已有测试的回归。
25
+
26
+ **建议**:
27
+ - 要么在 workflow-engine.ts 中添加 `deleteCheckpointFile` 的导出(如果实际需要)
28
+ - 要么从测试中移除这个断言(如果该函数已被移除或从未存在)
29
+ - 由于本次任务是"只修 bug,不做重构",建议作为独立 issue 处理
30
+
31
+ ### M2. `executeLoopGroup` 中 retry 后未递归检查 agentResult
32
+
33
+ **文件**: `extensions/workflow-engine.ts` 第 1204 行
34
+ **问题**: 在非 full-auto 模式下,exitCode 检查分支中 retry 逻辑是:
35
+ ```typescript
36
+ agentResult = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, ...);
37
+ ```
38
+ retry 后**没有**再次检查 `agentResult.exitCode`。如果 retry 的 agent 仍然异常退出(例如退出码 1),流程会无检查地进入 review 阶段。这与超时处理的模式不一致——超时处理在 retry 后会检查 `isTimeoutResult(agentResult)` 并 fallback。
39
+
40
+ **严重程度**: 中等 — 边界情况(连续两次退出码非零),但可能导致错误结果被静默接受。
41
+
42
+ **建议**: retry 后添加 exitCode 检查:
43
+ ```typescript
44
+ agentResult = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, ...);
45
+ // retry 后再次检查
46
+ if (agentResult.exitCode !== 0 && !isTimeoutResult(agentResult)) {
47
+ // fallback: 跳过此步骤
48
+ state.status = "skipped";
49
+ return;
50
+ }
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 🟢 低优先级问题
56
+
57
+ ### L1. `cleanupWidget` 中的 `_workflowRunning = false` 与 `executeWorkflowBackground` 的 `_workflowRunning = false` 重复
58
+
59
+ **文件**: `extensions/workflow-engine.ts` 第 803 行 和第 1450 行
60
+ **问题**: `executeWorkflowBackground` 在步骤循环完成后设置 `_workflowRunning = false`(第 1450 行),然后新建的定时器在 5 秒后调用 `cleanupWidget()` 再次设置 `_workflowRunning = false`(第 803 行)。虽然不会影响正确性(重复赋值相同值),但逻辑上冗余。
61
+
62
+ **建议**: 可以移除 `executeWorkflowBackground` 中的 `_workflowRunning = false`,让 `cleanupWidget` 统一管理该变量,避免逻辑分散。
63
+
64
+ ### L2. 测试中 `simulateBugAFix` 的 retry 返回语义不精确
65
+
66
+ **文件**: `tests/test-workflow-engine-bugs.mjs` 第 147-174 行
67
+ **问题**: `simulateBugAFix` 函数在非 full-auto 模式下返回 `"retry"`,但这个返回值没有区分"用户选择了跳过"、"用户选择了重试"、"用户选择了取消"三种情况。虽然作为模拟测试够用,但语义不够精确。
68
+
69
+ **建议**: 可以用更精确的返回值(如 `"retry"`、`"skip"`、`"cancel"`),或使用对象形式返回。
70
+
71
+ ---
72
+
73
+ ## 维护建议
74
+
75
+ 1. 修复测试 `test-workflow-engine.mjs` 中 deleteCheckpointFile 的断言——这会导致 CI 失败。
76
+ 2. 考虑在 retry 后添加二次 exitCode 检查(M2),以增强鲁棒性。
77
+ 3. 整体修复方案符合"最小化改动"要求,代码阅读性和可维护性好。
78
+
79
+ ---
80
+
81
+ ## 测试结果
82
+
83
+ - ✅ `node tests/test-workflow-engine-bugs.mjs` — **50/50 通过**(新测试全部通过)
84
+ - ⚠️ `node tests/test-workflow-engine.mjs` — **73/74 通过**(1 个 pre-existing 失败,非本次改动导致)
85
+ - ✅ `git diff HEAD` 确认变更仅限于计划中指定的范围
86
+
87
+ ---
88
+
89
+ ## 审查结论
90
+
91
+ 本次修复正确地解决了两个 Bug。代码实现与实施计划一致,没有引入回归。存在 2 个中等问题和 2 个低优先级问题,但没有严重问题。
@@ -0,0 +1,191 @@
1
+ # 代码审查报告: Workflow loopCount UI 同步 & 超时时间分离修复
2
+
3
+ **审查日期**: 2026-05-21
4
+ **审查范围**: `extensions/workflow-engine.ts`, `extensions/dev-prompts.ts`, `extensions/ui-helpers.ts`, `tests/test-loopcount-timeout-fix.mjs`
5
+ **git diff**: 已实现的功能修复(3 个文件修改 + 1 个测试文件新增)
6
+
7
+ ## 总体评价
8
+
9
+ 代码改动整体方向正确,核心功能修复已实现。但发现 2 个中等严重程度的问题和 2 个低优先级问题。
10
+
11
+ ---
12
+
13
+ ## M1. 在运行 `initWidget` 初始化所有步骤时(第 1629 行),loop-group 步骤的 `timeoutMs` 被传入 widget
14
+
15
+ **严重度**: medium
16
+ **文件**: `extensions/workflow-engine.ts`
17
+ **位置**: 第 1628-1631 行
18
+
19
+ ```typescript
20
+ updateWidgetStep(i, steps[i]!.label, isDoneState ? "done" : "pending", {
21
+ maxLoops: steps[i]!.maxLoops,
22
+ timeoutMs: steps[i]!.timeoutMs, // <-- loop-group 步骤的 timeoutMs 被传入
23
+ });
24
+ ```
25
+
26
+ **问题描述**:
27
+ 根据设计评审第 8 问的决策,"loop-group 行不显示超时时间,在 worker 和 reviewer 的 sub-step 中分别显示各自的超时时间"。但 `initWidget` 阶段对所有步骤(包括 loop-group)迭代调用了 `updateWidgetStep`,且传入了 `timeoutMs`。
28
+
29
+ 虽然在 `executeWorkflowBackground` 的 running 和 done 状态更新中已经修复了(使用 `step.type === "loop-group" ? undefined : step.timeoutMs`),但初始 pending 状态的设置没有过滤 loop-group 步骤。这意味着 widget 面板在启动瞬间(步骤还未开始执行时),loop-group 行会短暂显示超时时间。
30
+
31
+ **影响**:
32
+ - 用户启动工作流后,在步骤进入 running 状态之前,会看到 loop-group 行有超时显示
33
+ - 这与设计意图不一致
34
+
35
+ **修复建议**:
36
+ ```typescript
37
+ updateWidgetStep(i, steps[i]!.label, isDoneState ? "done" : "pending", {
38
+ maxLoops: steps[i]!.maxLoops,
39
+ timeoutMs: steps[i]!.type === "loop-group" ? undefined : steps[i]!.timeoutMs,
40
+ });
41
+ ```
42
+
43
+ ---
44
+
45
+ ## M2. loopCount 的"第 1 次循环"在 `buildWidgetLines` 中存在隐藏的重复逻辑
46
+
47
+ **严重度**: medium
48
+ **文件**: `extensions/ui-helpers.ts`
49
+ **位置**: 第 462-471 行
50
+
51
+ ```typescript
52
+ let loopStr = "";
53
+ if (s.loopCount != null && s.loopCount > 0) {
54
+ loopStr = dim(theme, ` · 第 ${s.loopCount} 次循环`);
55
+ } else if (s.maxLoops != null) {
56
+ if (isRunning) {
57
+ // Immediately show 第 1 次循环 when loop-group starts
58
+ loopStr = dim(theme, ` · 第 1 次循环`);
59
+ } else if (isPending) {
60
+ loopStr = dim(theme, ` · 第 0 次循环`);
61
+ }
62
+ }
63
+ ```
64
+
65
+ **问题描述**:
66
+ 这个 `else if (isRunning)` 分支原本是修复前的"硬编码显示第 1 次循环"的残留。虽然代码逻辑在实现层面并无错误,但在已实施第 1 轮修复(`executeLoopGroup` 中 `loopCount++` 后立即 `updateWidgetStep`)之后:
67
+
68
+ 1. 当步骤第一次进入 running 状态时,`executeWorkflowBackground` 先调用 `updateWidgetStep` 将 `loopCount` 设为 `undefined`(初始调用没传 `loopCount`)
69
+ 2. 紧接着 `executeLoopGroup` 的 while 循环中,`runAgentWithProgress` 运行结束后 `loopCount++` 并调用 `updateWidgetStep` 传入 `loopCount: 1`
70
+
71
+ 因此在第 1 步与第 2 步之间的短暂时间窗口,UI 确实会通过 `else if (isRunning)` 分支显示"第 1 次循环"——但这**恰好是正确的**。
72
+
73
+ 然而问题是:**如果 `loopCount` 已通过 `updateWidgetStep` 传入**(即 `s.loopCount` 不为 null),那么这段 `else if` 分支永远不会被执行,因为它走的是上面的 `if (s.loopCount != null && s.loopCount > 0)` 分支。
74
+
75
+ 真正的风险在于:**假如 `updateWidgetStep` 因为某种竞态条件(race condition)没有及时更新 `s.loopCount`**,这个 `else if` 分支会掩盖问题,让错误很难调试。它相当于一个静默的"兜底"逻辑,可能隐藏真实的 loopCount 同步 bug。
76
+
77
+ **影响**:
78
+ - 降低可调试性:掩盖了可能的同步问题
79
+ - 代码可读性降低:混合了修复前和修复后的逻辑
80
+ - 与"直接从 `s.loopCount` 读取显示"的设计决策不完全一致
81
+
82
+ **修复建议**:
83
+ 移除 `else if (isRunning)` 分支,完全依赖 `s.loopCount`。修改为:
84
+
85
+ ```typescript
86
+ let loopStr = "";
87
+ if (s.loopCount != null && s.loopCount > 0) {
88
+ loopStr = dim(theme, ` · 第 ${s.loopCount} 次循环`);
89
+ } else if (s.maxLoops != null) {
90
+ // 仅在 pending 时显示"第 0 次循环"
91
+ // running 状态时 loopCount 应由 executeLoopGroup 通过 updateWidgetStep 更新
92
+ if (isPending) {
93
+ loopStr = dim(theme, ` · 第 0 次循环`);
94
+ }
95
+ }
96
+ ```
97
+
98
+ 这样可以让 running 且 loopCount 尚未更新时的短暂窗口不显示循环次数(或者显示空字符串),给 `updateWidgetStep` 一个可观察的时间窗口来更新。
99
+
100
+ ---
101
+
102
+ ## L1. 测试断言不够健壮 —— 使用字符串包含检测而非 AST 解析
103
+
104
+ **严重度**: low
105
+ **文件**: `tests/test-loopcount-timeout-fix.mjs`
106
+ **位置**: 多处
107
+
108
+ **问题描述**:
109
+ 测试大量使用 `assertIncludes` 进行字符串包含检测。例如:
110
+
111
+ ```javascript
112
+ assertIncludes("1.1 executeLoopGroup 中有 loopCount++ 后立即更新 UI", weContent, "// 立即更新 UI 显示当前循环次数");
113
+ ```
114
+
115
+ 这种方式有几个问题:
116
+ 1. 如果注释文本被修改(拼写修正、翻译等),测试会假阴性(false negative)
117
+ 2. 容易被注释中的无关字符串匹配到意外命中
118
+ 3. 无法验证代码的实际执行流——只验证了源代码文本存在
119
+
120
+ 更严重的是:测试检测的是**注释文本**而非实际代码。注释修改不影响功能,但测试会失败。
121
+
122
+ **修复建议**:
123
+ - 对于注释检测,改为检测更独特的代码模式(如 `updateWidgetStep(stepIndex, step.label, "running", {`)
124
+ - 或直接检测变量名模式,例如 `loopCount,\n.*maxLoops:` 等
125
+ - 对于接口定义检测,使用正则匹配 AST 级别模式(如 `reviewTimeoutMs`/`WorkflowStepDef` 上下文)
126
+
127
+ ---
128
+
129
+ ## L2. `setWidgetSubStepStatus` 重置后,sub-step 的 `detail` 字段未清除
130
+
131
+ **严重度**: low
132
+ **文件**: `extensions/workflow-engine.ts`
133
+ **位置**: 第 1186-1187 行
134
+
135
+ ```typescript
136
+ setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
137
+ setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
138
+ ```
139
+
140
+ **问题描述**:
141
+ 每次循环开始时,sub-step 的状态被重置为 `"pending"`。但 `setWidgetSubStepStatus` 只更新 `sub.status` 字段,不清除 `sub.detail`、`sub.tools`、`sub.outputs` 等字段。
142
+
143
+ 查看 `setWidgetSubStepStatus` 的实现:
144
+ ```typescript
145
+ function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: WorkflowSubStepWidgetState["status"]): void {
146
+ const step = _widgetSteps[stepIndex];
147
+ if (!step) return;
148
+ const sub = step.subSteps?.find(s => s.agent === agentName);
149
+ if (sub) {
150
+ sub.status = status;
151
+ refreshWidget();
152
+ }
153
+ }
154
+ ```
155
+
156
+ 当第二次循环开始时:
157
+ - sub-step 状态变成 `"pending"`
158
+ - 但在 `buildWidgetLines` 中,`isSubPending` 为 true,因此 `childItems` 只显示 `"正在排队"`——这恰好正确,因为待机状态不显示 `detail`
159
+ - 当 sub-step 再次进入 running 状态时,`runAgentWithProgress` 会重新设置 `detail`
160
+
161
+ 所以当前的 bug 不严重,**UI 显示是正确的**。但如果未来修改了 `buildWidgetLines` 中 pending 状态的渲染逻辑(例如显示 sub-step 的 detail),就可能显示上一轮循环的超时信息。
162
+
163
+ **修复建议**:
164
+ 在 `setWidgetSubStepStatus` 中增加可选清除 detail 的功能,或在 `executeLoopGroup` 重置时显式清除:
165
+
166
+ ```typescript
167
+ function resetWidgetSubStep(stepIndex: number, agentName: string): void {
168
+ const step = _widgetSteps[stepIndex];
169
+ if (!step) return;
170
+ const sub = step.subSteps?.find(s => s.agent === agentName);
171
+ if (sub) {
172
+ sub.status = "pending";
173
+ sub.detail = undefined;
174
+ sub.tools = [];
175
+ sub.outputs = [];
176
+ refreshWidget();
177
+ }
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## 总结
184
+
185
+ 本次修复正确地解决了三个核心问题:
186
+ 1. ✅ **loopCount UI 同步**:`executeLoopGroup` 中 `loopCount++` 后立即调用 `updateWidgetStep`
187
+ 2. ✅ **reviewer 独立超时**:`WorkflowStepDef` 新增 `reviewTimeoutMs` 字段,reviewer 使用独立超时
188
+ 3. ✅ **默认超时值**:worker=30min, trimmer=20min, reviewer=15min
189
+ 4. ✅ **loop-group 行不显示 timeout**:在 running/done 状态更新中过滤了 loop-group 步骤
190
+
191
+ 发现的中等问题(M1, M2)不会导致实际功能错误,但可能影响 UI 一致性和可维护性。
@@ -0,0 +1,189 @@
1
+ # 代码审查报告: Esc 双击确认停止工作流
2
+
3
+ **审查日期**: 2026-05-21
4
+ **审查范围**: `extensions/workflow-engine.ts`(Esc 处理逻辑)
5
+ **审查变更**: `git diff HEAD` — 仅 `extensions/workflow-engine.ts` 第 1706-1726 行
6
+
7
+ ## 总体评价
8
+
9
+ 本次改动代码方向正确,实现了任务要求的"第一次 Esc 显示提示、5s 内第二次 Esc 才确认取消"的功能。
10
+ 代码量极小(约 15 行改动),符合"最小改动"原则。
11
+ 但发现 **1 个严重问题(critical)** 和 **2 个低优先级问题**。
12
+
13
+ ---
14
+
15
+ ## 🔴 严重问题
16
+
17
+ ### C1. cancelWorkflow() 回调中重新订阅的 terminal input 未解绑
18
+
19
+ **严重度**: critical
20
+ **文件**: `extensions/workflow-engine.ts`
21
+ **位置**: 第 1698-1702 行(cleanup 定时器设置)/ 第 1709-1727 行(onTerminalInput 订阅)
22
+
23
+ **问题描述**:
24
+
25
+ `cancelWorkflow()` 的执行路径如下:
26
+
27
+ 1. 用户第二次按 Esc → `cancelWorkflow()` 被调用
28
+ 2. `cancelWorkflow()` → `_onCancelWorkflow?.()` → 回调执行:
29
+ - `_workflowAbortController?.abort()` — 中止工作流
30
+ - `_workflowRunning = false`
31
+ - 保存 checkpoint
32
+ - 设置 `_cleanupTimer = setTimeout(() => { cleanupWidget(); }, 5000);`
33
+
34
+ 3. 5 秒后 `cleanupWidget()` 执行:
35
+ - 清理 `_terminalInputUnsubscribe()`
36
+ - `_terminalInputUnsubscribe = null`
37
+
38
+ **问题在于**:在第 2 步中 5 秒的清理延迟期间,`_terminalInputUnsubscribe` **仍然有效**,回调依然能触发。虽然 `_workflowRunning = false` 阻止了进入 Esc 处理分支(第 1714 行的 `if (_workflowRunning && ...)` 为 false),所以不会造成错误取消,但存在一个**更隐蔽的问题**:
39
+
40
+ 如果用户在 5 秒延迟窗口内**重新开始一个新的工作流**(通过 `runWorkflow` 另一个调用),第 1711 行会创建一个新的 `_lastEscPressTime` 闭包变量,并重新赋值 `_terminalInputUnsubscribe`:
41
+
42
+ ```typescript
43
+ // 新工作流
44
+ let _lastEscPressTime = 0; // 新的闭包变量
45
+ _terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => { ... }); // 覆盖旧的
46
+ ```
47
+
48
+ 旧的回调**永远不会被解绑**(因为旧的 `_terminalInputUnsubscribe` 引用被覆盖了,且 `cleanupWidget` 还没运行)。这意味着旧定的 5 秒计时器到期后,`cleanupWidget` 运行时会:
49
+ - 调用已经过期的 `_terminalInputUnsubscribe`(指向新工作流的订阅)
50
+ - 新工作流的 Esc 监听被错误解绑!
51
+
52
+ **根因分析**:
53
+ `setWorkflowCancelCallback` 的回调中使用 `setTimeout(() => cleanupWidget(), 5000)` 延迟清理,而新工作流可能在延迟期间启动,导致 `_terminalInputUnsubscribe` 被覆盖。这不是此改动引入的新问题——它是整个 `setTimeout` 延迟清理设计本身就存在的竞态问题。
54
+
55
+ 但在没有此改动前,这个问题较少暴露,因为旧代码中单次 Esc 就立即取消了。现在加了双击确认后,用户可能在第一次 Esc 提示后**无意中触发这个竞态**:
56
+
57
+ 1. 工作流 A 正在运行
58
+ 2. 用户按一次 Esc → 显示提示
59
+ 3. 用户等了一段时间,又按了一次 Esc(但已超过 5s,重置为第一次)
60
+ 4. 用户再按一次 Esc(第二次,仍在工作流 A)
61
+ 5. 工作流 A 被取消,状态设为 `_workflowRunning = false`,等待 5s 清理
62
+ 6. 用户立即启动工作流 B
63
+ 7. 工作流 B 的 `_terminalInputUnsubscribe` 覆盖了旧的
64
+ 8. 工作流 A 的 5 秒定时器到期,`cleanupWidget()` 解绑了工作流 B 的 Esc 监听
65
+ 9. 工作流 B 运行时无法通过 Esc 取消
66
+
67
+ **影响**:竞态条件触发的概率较低(需要在特定时间窗口内快速操作),但一旦触发,后一个工作流将失去 Esc 取消功能。
68
+
69
+ **修复方案**:
70
+
71
+ **方案一(推荐,最小改动)**:在 `setWorkflowCancelCallback` 的回调中立即清理 Esc 监听,而不是等到 5 秒后。
72
+
73
+ 在第 1661 行附近,`_cleanupTimer = setTimeout(() => { ... cleanupWidget(); }, 5000)` 之前,立即执行:
74
+
75
+ ```typescript
76
+ // 在 cancel 回调中立即清理 Esc 监听,避免竞态
77
+ if (_terminalInputUnsubscribe) {
78
+ _terminalInputUnsubscribe();
79
+ _terminalInputUnsubscribe = null;
80
+ }
81
+ ```
82
+
83
+ 这将确保:
84
+ - cancel 后 Esc 监听立即解绑
85
+ - 5 秒后 `cleanupWidget()` 中再次调用 `_terminalInputUnsubscribe?.()`(此时已是 null,安全)
86
+ - 新的工作流不会受到旧监听器残留的影响
87
+
88
+ **方案二(更强健)**:使用独立的 `_cancelPending` 标志,在 `onTerminalInput` 回调中检查并跳过已经取消的工作流的残留事件。但方案一更简单且是本改动即可独立修复的。
89
+
90
+ **注意**:此问题虽然是修改前就存在的(延迟清理设计),但本次改动增加了用户使用 Esc 交互的次数(第一次 Esc→提示,第二次 Esc→确认),从而**增加了进入上述竞态时间窗口的概率**。因此将其标记为严重等级。
91
+
92
+ ---
93
+
94
+ ## 🟢 低优先级问题
95
+
96
+ ### L1. 超过 5s 后按 Esc,提示内容不会再出现
97
+
98
+ **文件**: `extensions/workflow-engine.ts` 第 1711-1726 行
99
+
100
+ **问题描述**:
101
+ 需求描述为:"俩次ecs间隔<5s才退出,5s之后,提示内容"再次按下ecs键,停止Workflow"去掉,需要重新监听ecs按下和重新计时。"
102
+
103
+ 当前实现中,超过 5s 后再次按 Esc 的逻辑是这样的:
104
+ 1. `_lastEscPressTime` 有旧值(5s 前的)
105
+ 2. `now - _lastEscPressTime >= 5000` 为 true
106
+ 3. 由于 `_lastEscPressTime > 0` **且** `now - _lastEscPressTime < 5000` 为 false
107
+ 4. 走 else 分支:`_lastEscPressTime = now; ctx.ui.notify(...)`
108
+
109
+ **这实际上是正确的**——超过 5s 后按 Esc,会重新记录时间并显示提示。所以功能上没有问题。
110
+
111
+ 但边界情况是:如果 `now - _lastEscPressTime` 恰好等于 5000ms(即用户在最后一次提示后刚好 5.000 秒按了 Esc),则 `< 5000` 的检查不通过,但 `_lastEscPressTime > 0` 仍然成立。代码会走到 else 分支,重置时间并显示提示——这符合"重新监听"的语义,**正确无误**。
112
+
113
+ **结论**: L1 不是问题,移除该条目。
114
+
115
+ ### L2. notify 消息中英文混合
116
+
117
+ **文件**: `extensions/workflow-engine.ts` 第 1725 行
118
+
119
+ **问题描述**:
120
+ 提示消息为 `"再次按下 Esc 键,停止 Workflow"`,其中 `Workflow` 是英文单词,其余为中文。该代码库中其他消息使用统一中文化(如 `"⏹️ 用户取消工作流"`)。
121
+
122
+ 建议统一为:"再次按下 Esc 键,停止工作流"(将 Workflow 改为中文)。
123
+
124
+ 这仅是一个风格建议,不影响功能。
125
+
126
+ **建议**: 将 `"Workflow"` 改为 `"工作流"`。
127
+
128
+ ### L3. 测试覆盖缺失
129
+
130
+ **文件**: 无对应测试文件
131
+
132
+ **问题描述**:
133
+ 现有 7 个测试文件中,没有任何测试覆盖 Esc 双击确认逻辑。无法验证该功能在代码重构或未来改动时不被破坏。
134
+
135
+ **建议**:
136
+ 在 `tests/` 下新增测试文件(如 `test-esc-double-press.mjs`),至少覆盖以下场景:
137
+ 1. 首次 Esc → 不取消,显示提示(检查 notify 调用)
138
+ 2. 5s 内第二次 Esc → 取消(检查 cancelWorkflow 调用)
139
+ 3. 超过 5s 后按 Esc → 重置(检查 notify 再次调用)
140
+ 4. 工作流非运行状态时 Esc → 无操作
141
+ 5. 连续 Esc 时 _lastEscPressTime 的时间戳更新逻辑
142
+
143
+ 由于 `onTerminalInput` 回调依赖 `ctx.ui`,可隔离测试回调的纯逻辑部分(mock `matchesKey`、`_workflowRunning`、`_workflowAbortController` 等)。
144
+
145
+ ---
146
+
147
+ ## 变更的正确性验证
148
+
149
+ ### ✅ 功能正确性
150
+ - 首次 Esc → 记录时间、显示提示、`return { consume: true }`(阻止默认行为) ✅
151
+ - 5s 内第二次 Esc → 调用 `cancelWorkflow()`、重置时间 ⏰ ≪-- C1 竞态风险
152
+ - 超过 5s 按 Esc → 重置为首次状态 ✅
153
+ - 非工作流运行时的 Esc → `return undefined`(不消费事件,允许其他处理) ✅
154
+ - `_lastEscPressTime` 是函数块级 `let` 变量,作用域正确 ✅
155
+ - 回调使用 `_workflowRunning + _workflowAbortController` 双重守卫 ✅
156
+
157
+ ### ✅ 与实施计划的一致性
158
+ - ✅ 引入 `_lastEscPressTime` 变量(函数块作用域)
159
+ - ✅ 第一次按 Esc → 记录时间并显示提示
160
+ - ✅ 在 5s 内再次按 Esc → 执行取消操作
161
+ - ✅ 超过 5s -> 重置为第一次状态
162
+ - ✅ 修改仅限于 `runWorkflow` 函数内的 `onTerminalInput` 回调
163
+ - ✅ 约 15 行改动量
164
+
165
+ ### ⚠️ 与需求的一致性
166
+ - ✅ "再次按下ecs键,停止Workflow" 提示显示
167
+ - ✅ 两次 Esc 间隔 < 5s 才退出
168
+ - ⚠️ 5s 之后,提示内容消失,重新监听 — 功能实现正确
169
+ - ⚠️ 不能破坏原有功能 — 需要确认 C1 竞态问题
170
+
171
+ ### ✅ 回归测试
172
+ - `node tests/test-workflow-engine-bugs.mjs` — **50/50 通过** ✅
173
+ - `node tests/test-workflow-engine.mjs` — **74/75 通过**(1 个 pre-existing `deleteCheckpointFile` 失败,非本次改动导致) ✅
174
+
175
+ ---
176
+
177
+ ## 严重等级汇总
178
+
179
+ | 等级 | 数量 | 说明 |
180
+ |------|------|------|
181
+ | 🔴 critical | 1 | cancelWorkflow 回调延迟清理导致的竞态 |
182
+ | 🟡 medium | 0 | — |
183
+ | 🟢 low | 2 | 中英文混合、测试覆盖缺失 |
184
+
185
+ ---
186
+
187
+ ## 总结
188
+
189
+ 本次 Esc 双击确认功能实现正确,核心逻辑符合需求。唯一严重问题是 `cancelWorkflow` 回调中的 5 秒延迟清理设计(第 1698 行)与 `_terminalInputUnsubscribe` 的覆盖之间存在竞态条件,可能导致后启动的工作流失去 Esc 取消功能。建议在 cancel 回调中立即清理 Esc 监听器,而不是等 5 秒后的 `cleanupWidget()` 再做。(此问题虽在改动前已存在,但本次改动增加了进入竞态时间窗口的概率。)