@ghyper9023/pi-dev-workflow 0.4.2 → 0.5.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 (60) hide show
  1. package/.doc/AGENT-FRONTMATTER-REFERENCE.md +198 -0
  2. package/.version/RELEASE-v0.4.2.md +31 -0
  3. package/.version/RELEASE-v0.4.3.md +42 -0
  4. package/.version/RELEASE-v0.5.0.md +82 -0
  5. package/README.md +23 -3
  6. package/agents/git-agent.md +7 -0
  7. package/agents/grill/dev-doc-grill-agent.md +7 -0
  8. package/agents/grill/dev-fix-grill-agent.md +7 -0
  9. package/agents/grill/dev-grill-agent.md +7 -0
  10. package/agents/grill/dev-perf-grill-agent.md +7 -0
  11. package/agents/grill/dev-prd-agent.md +7 -0
  12. package/agents/grill/dev-refactor-grill-agent.md +7 -0
  13. package/agents/grill/dev-test-grill-agent.md +7 -0
  14. package/agents/review-agent.md +7 -0
  15. package/agents/workflow/docWriter-agent.md +13 -2
  16. package/agents/workflow/planner-agent.md +16 -3
  17. package/agents/workflow/reviewer-agent.md +21 -4
  18. package/agents/workflow/trimmer-agent.md +12 -1
  19. package/agents/workflow/worker-agent.md +17 -2
  20. package/extensions/grill-me-agent.ts +58 -8
  21. package/extensions/sub-agents.ts +133 -16
  22. package/extensions/ui-helpers.ts +27 -27
  23. package/extensions/workflow-engine.ts +323 -16
  24. package/package.json +1 -1
  25. package/tests/test-workflow-engine-bugs.mjs +317 -0
  26. package/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +0 -14
  27. package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +0 -58
  28. package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +0 -13
  29. package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +0 -13
  30. package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +0 -13
  31. package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +0 -13
  32. package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +0 -94
  33. package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +0 -150
  34. package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +0 -215
  35. package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +0 -240
  36. package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +0 -253
  37. package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +0 -137
  38. package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +0 -258
  39. package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +0 -196
  40. package/.pi-dev-output/pi-review/md/review-20260520-100000.md +0 -91
  41. package/.pi-dev-output/pi-review/md/review-20260521-140000.md +0 -191
  42. package/.pi-dev-output/pi-review/md/review-20260521-190000.md +0 -189
  43. package/.pi-dev-output/pi-review/md/review-20260521-204500.md +0 -241
  44. package/.pi-dev-output/pi-review/md/review-20260521-214500.md +0 -270
  45. package/.pi-dev-output/pi-review/md/review-20260521-215158.md +0 -214
  46. package/.pi-dev-output/pi-review/md/review-20260521-234500.md +0 -201
  47. package/.pi-dev-output/pi-review/md/review-20260521-235500.md +0 -422
  48. package/.pi-dev-output/pi-review/md/review-20260522-000000.md +0 -212
  49. package/.pi-dev-output/pi-review/md/review-20260522-003000.md +0 -377
  50. package/.pi-dev-output/pi-review/md/review-20260522-003500.md +0 -296
  51. package/.pi-dev-output/pi-workflow/checkpoint-20260520-153000-fix-workflow-engine-bugs.json +0 -108
  52. package/.pi-dev-output/pi-workflow/checkpoint-20260521-113000-fix-loopcount-timeout.json +0 -402
  53. package/.pi-dev-output/pi-workflow/checkpoint-20260521-1730-grill-input-wrap-back-fix.json +0 -447
  54. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230000-fix-timeout-display-loopcount-gitdiff.json +0 -708
  55. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230500-esc-double-press-confirm-workflow.json +0 -365
  56. package/.pi-dev-output/pi-workflow/checkpoint-20260521-235000-fix-gitdiff-loopcount.json +0 -395
  57. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +0 -30
  58. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +0 -49
  59. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +0 -59
  60. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi5r22.json +0 -76
@@ -1,253 +0,0 @@
1
- # 修复超时时间显示位置、循环次数计数与 git diff 解析 — 实施计划
2
-
3
- ## 概述
4
-
5
- 修复三个独立问题:
6
-
7
- 1. **超时时间显示位置错误**:当前超时时间(如 `超时时间60m`)被显示在步骤主行(step 行),而非子代理行(sub-step 行)。预期行为是 `|__ worker · (52.6s/超时时间60m )`。
8
- 2. **循环次数(loopCount)计数不显示**:上次 commit (01413c9) 修改了 loopCount 显示逻辑,导致 loop-group 启动后 `第 1 次循环` 不再显示。原因为 commit 中 `buildWidgetLines` 删除了 `isRunning` 状态下显示循环次数的逻辑,但 `executeLoopGroup` 中的 `updateWidgetStep` 调用在 loopCount 更新后未在 widget UI 上正确体现。
9
- 3. **getGitDiffChanges 中 git diff 解析使用正则而非简单 string 拆分**:当前正则 `^([MAD])\s+(.+)$` 在解析 `git diff --name-status` 的输出(格式为 `M\t.gitignore` 或 `M .gitignore`)时,可能因不匹配制表符而丢失变更。实际输出非常规整,应使用简单的字符串拆分。
10
-
11
- ## 文件清单
12
-
13
- ### 修改文件
14
-
15
- | 文件路径 | 改动描述 | 风险等级 |
16
- |---------|---------|---------|
17
- | `extensions/ui-helpers.ts` | 修改 `buildWidgetLines` 中的子代理行渲染,在 `status` 图标和 agent 名称后添加 `(当前计时/超时时间Xm)`;恢复步骤行中 `isRunning` 状态下 `loopCount` 的显示逻辑 | 低 |
18
- | `extensions/workflow-engine.ts` | 修改 `getGitDiffChanges` 中 `git diff --name-status` 和 `git status --porcelain` 的解析逻辑,用简单字符串拆分替代正则匹配 | 低 |
19
-
20
- ## 实施步骤
21
-
22
- ### 步骤 1:修复子代理行的超时时间显示位置(ui-helpers.ts)
23
-
24
- - **前置条件**:无
25
- - **改动文件**:`extensions/ui-helpers.ts`
26
- - **改动内容**:
27
-
28
- **1a. 在 `buildWidgetLines` 的子代理渲染循环中,在 agent 行后添加计时和超时信息。**
29
-
30
- 当前子代理行渲染(ui-helpers.ts ~第539行):
31
- ```
32
- lines.push(`${agentIndent}${agentConnector} ${subIcon} ${sub.agent} ·`);
33
- ```
34
-
35
- 需要修改为:
36
- ```
37
- let subDurStr = "";
38
- let subTimeoutStr = "";
39
- // 计算 sub-step 的当前时长
40
- if (sub.startedAt) {
41
- const elapsedMs = Date.now() - sub.startedAt;
42
- subDurStr = dim(theme, ` (${formatDurationFull(elapsedMs)}`);
43
- } else if (isSubRunning) {
44
- subDurStr = dim(theme, ` (0s`);
45
- }
46
- // 从 sub.detail 提取超时信息(detail 已在 runAgentWithProgress 中设置为 "超时时间60m")
47
- if (sub.detail) {
48
- subTimeoutStr = dim(theme, `/${sub.detail}`);
49
- }
50
- const subDurClose = subDurStr ? dim(theme, ")") : "";
51
-
52
- lines.push(`${agentIndent}${agentConnector} ${subIcon} ${sub.agent} ·${subDurStr}${subTimeoutStr}${subDurClose}`);
53
- ```
54
-
55
- **1b. 步骤行中的超时时间移除/调整**
56
-
57
- 当前步骤行(step 行)有 `durStr + timeout + durClose`,对于 loop-group 步骤,`timeoutMs` 在 workflow-engine.ts 中已被设置为 `undefined`(`step.type === "loop-group" ? undefined : step.timeoutMs`),所以步骤行不会显示超时——这部分逻辑保持不变。
58
-
59
- 但对于非 loop-group 步骤,步骤行仍显示 `(1m59s/超时时间15m)`,这是正确的行为,无需改动。
60
-
61
- **关键设计决策**:让子代理行显示 `(当前计时/超时时间)`,步骤行对于 loop-group 不显示超时(因为 loop-group 的超时已在子代理级别体现)。
62
-
63
- - **验证方式**:运行 `node tests/test-loopcount-timeout-fix.mjs`,确认现有测试全部通过;手动检查 widget 渲染逻辑。
64
-
65
- ### 步骤 2:修复循环次数(loopCount)显示(ui-helpers.ts)
66
-
67
- - **前置条件**:步骤 1 完成
68
- - **改动文件**:`extensions/ui-helpers.ts`
69
- - **改动内容**:
70
-
71
- **2a. 在 `buildWidgetLines` 中,恢复 `isRunning` 状态下显示循环次数的逻辑。**
72
-
73
- 当前代码(ui-helpers.ts ~第471行):
74
- ```typescript
75
- if (s.loopCount != null && s.loopCount > 0) {
76
- loopStr = dim(theme, ` · 第 ${s.loopCount} 次循环`);
77
- } else if (s.maxLoops != null) {
78
- // 仅在 pending 时显示"第 0 次循环"
79
- // running 状态时 loopCount 应由 executeLoopGroup 通过 updateWidgetStep 更新
80
- if (isPending) {
81
- loopStr = dim(theme, ` · 第 0 次循环`);
82
- }
83
- }
84
- ```
85
-
86
- 这里的问题是:当 loop-group step 处于 `isRunning` 状态且 `loopCount` 已通过 `updateWidgetStep` 设置(如 `loopCount=1`)时,`s.loopCount != null && s.loopCount > 0` 条件应匹配,所以 `第 1 次循环` **应该**显示。但实际运行中,`updateWidgetStep` 在 `executeLoopGroup` 中的调用可能没有被正确触发。
87
-
88
- 排查 `executeLoopGroup` 中的代码(workflow-engine.ts ~第1250行):
89
- ```typescript
90
- loopCount++;
91
- // 立即更新 UI 显示当前循环次数
92
- state.loopCount = loopCount;
93
- updateWidgetStep(stepIndex, step.label, "running", {
94
- loopCount,
95
- maxLoops: step.maxLoops,
96
- startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
97
- });
98
- ```
99
-
100
- 这里 `updateWidgetStep` 被调用时传入了 `"running"` 状态和 `loopCount`。调用 `updateWidgetStep` 会触发 `refreshWidget()`,然后 `buildWidgetLines` 读到的 `s.loopCount` 应为刚刚设置的值(例如 1),因此 `s.loopCount != null && s.loopCount > 0` 应成立,`第 1 次循环` 应显示。
101
-
102
- **根因分析**:问题在于 `updateWidgetStep` 的 `extra` 参数展开覆盖了 step widget state 中的 `loopCount`。在 `updateWidgetStep` 函数中:
103
- ```typescript
104
- _widgetSteps[index] = {
105
- ...existing,
106
- label,
107
- status,
108
- ...extra,
109
- };
110
- ```
111
-
112
- 这里 `extra` 包含 `{ loopCount, maxLoops, startedAt }`,这会将 `loopCount` 设置到 widget step 上。所以 `s.loopCount` 是有的。理论上代码是正确同步的。
113
-
114
- 但实际行为中,`executeLoopGroup` 的 `while` 循环中,**在 `loopCount++` 和 `updateWidgetStep` 执行之前**,reviewer 的 `runAgentWithProgress` 内部调用了 `refreshWidget()`(通过 `setWidgetSubStepStatus`),这意味着在第一次循环中,widget 在 loopCount 更新前被渲染了。但 `updateWidgetStep` 紧随其后就会更新。
115
-
116
- **真正的 bug**:在 `executeLoopGroup` 中,第一次循环的 `updateWidgetStep` 调用在 `loopCount++`(从 0 变为 1)之后。因此第一次循环完成后,`loopCount=1`,这能在 widget 上显示 `第 1 次循环`。但是,**当进入下一轮循环时**,`setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending")` 和 `setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending")` 被调用,这些调用触发 `refreshWidget()` 但**不会**更新 `loopCount`,所以 `loopCount` 仍然是上一次循环的值(如 1),所以在第二次循环的 worker 运行时,UI 显示的还是 `第 1 次循环`。
117
-
118
- 但实际上,根据需求描述:"第 0 次循环只在排队时候显示了,等 loop 组开始工作连第 1 次循环的提示都不见了"。这意味着 **完全看不到循环计数**。
119
-
120
- 更可能的原因是:**在 `executeLoopGroup` 中,第一次循环的 `updateWidgetStep` 调用位置不正确**。注意 `executeLoopGroup` 中执行流程:
121
-
122
- 1. `while (loopCount < maxLoops)` — 进入循环,`loopCount=0`
123
- 2. 重置 sub-step 为 pending
124
- 3. 运行 worker agent
125
- 4. 运行 reviewer agent
126
- 5. `loopCount++`(变成 1)
127
- 6. `state.loopCount = loopCount;`(设置为 1)
128
- 7. `updateWidgetStep(...)` — 这里设置 `loopCount=1`
129
-
130
- 在第 3 步运行 worker 期间,`runAgentWithProgress` 会多次调用 `refreshWidget()`(通过 `setWidgetSubStepStatus`、`addWidgetSubStepTool` 等)。这些 refresh 中,widget step 的 `loopCount` 是 **未定义**(`undefined`)的,因为 `updateWidgetStep` 还未被调用。所以在 worker 运行期间,`buildWidgetLines` 读到 `s.loopCount == null`,只看到 `s.maxLoops != null`,然后因为 `isRunning` 为 true,老的代码逻辑(在 commit 01413c9 之前的代码)会显示 `第 1 次循环`,但 commit 01413c9 删除了这个逻辑——**导致整个 worker/reviewer 运行期间 loopCount 完全不可见**。
131
-
132
- 也就是说,在 commit 01413c9 中,这部分代码被删除:
133
- ```
134
- - if (isRunning) {
135
- - // Immediately show 第 1 次循环 when loop-group starts
136
- - loopStr = dim(theme, ` · 第 1 次循环`);
137
- - }
138
- ```
139
-
140
- 移除这个逻辑的意图是"让 `executeLoopGroup` 通过 `updateWidgetStep` 管理 loopCount"。但当 worker/reviewer 运行时(第 3、4 步),`updateWidgetStep` 还未被调用(它在第 7 步才调用),所以 widget 没有 loopCount,也没有显示任何循环计数。
141
-
142
- 修复方案:**恢复 `isRunning` 状态下显示 `第 1 次循环` 的逻辑**(当 `s.loopCount` 为 null/undefined 时),或者更精确地说,在 `isRunning` 状态下,如果 `s.loopCount == null` 但 `s.maxLoops != null`,也显示 `第 1 次循环`。
143
-
144
- 同时,移除 `isRunning` 状态的限制条件:
145
- ```typescript
146
- if (s.loopCount != null && s.loopCount > 0) {
147
- loopStr = dim(theme, ` · 第 ${s.loopCount} 次循环`);
148
- } else if (s.maxLoops != null) {
149
- if (isRunning) {
150
- // 当 loop-group 开始运行时,即使 loopCount 尚未通过 updateWidgetStep 设置,
151
- // 也显示"第 1 次循环"(第 0 次循环仅用于 pending 状态)
152
- loopStr = dim(theme, ` · 第 1 次循环`);
153
- } else if (isPending) {
154
- loopStr = dim(theme, ` · 第 0 次循环`);
155
- }
156
- }
157
- ```
158
-
159
- 这样,当子代理刚刚开始运行时,即便 `loopCount` 还未更新,也能立即显示 `第 1 次循环`。
160
-
161
- - **验证方式**:运行 `node tests/test-loopcount-timeout-fix.mjs`;阅读修改后的代码确认逻辑正确
162
-
163
- ### 步骤 3:修复 `getGitDiffChanges` 中的 git diff 解析逻辑(workflow-engine.ts)
164
-
165
- - **前置条件**:无
166
- - **改动文件**:`extensions/workflow-engine.ts`
167
- - **改动内容**:
168
-
169
- **3a. 修改 `git diff --name-status` 的解析。**
170
-
171
- 当前使用正则:
172
- ```typescript
173
- const match = trimmed.match(/^([MAD])\s+(.+)$/);
174
- ```
175
-
176
- `git diff --name-status` 的实际输出格式为 `M\t.gitignore`(即 `status + 制表符 + 文件路径`)。上面的正则中 `\s+` 可以匹配制表符,**实际上不会有匹配问题**。但是根据用户反馈,正则会"识别出来一些无关东西,判断不出来是哪里来的"。
177
-
178
- 更健壮的方案:直接按制表符或空格拆分,取第一个字符为 status,其余为文件路径:
179
- ```typescript
180
- // Format: "M\tpath/to/file" (tab-separated) or "M path/to/file" (spaces)
181
- const firstSpace = trimmed.indexOf("\t");
182
- if (firstSpace < 0) {
183
- // Try multiple spaces
184
- const statusChar = trimmed[0];
185
- if (statusChar === "M" || statusChar === "A" || statusChar === "D") {
186
- const rest = trimmed.slice(1).trim();
187
- if (rest) {
188
- changes.push({ status: statusChar as "M" | "A" | "D", path: rest });
189
- }
190
- }
191
- } else {
192
- const status = trimmed.slice(0, firstSpace).trim();
193
- const path = trimmed.slice(firstSpace + 1).trim();
194
- if (status && path && !seen.has(path)) {
195
- seen.add(path);
196
- changes.push({ status: status as "M" | "A" | "D", path });
197
- }
198
- }
199
- ```
200
-
201
- **更简单的方案**:由于 `git diff --name-status` 的输出格式保证是 `X\tfilepath\n`,最简单的做法是按 `\t` 拆分:
202
- ```typescript
203
- // git diff --name-status 输出格式:X\tfilepath\n
204
- // 直接按制表符拆分,X 是第一个字符,filepath 是第二部分
205
- const parts = trimmed.split("\t");
206
- if (parts.length === 2) {
207
- const status = parts[0]!.trim();
208
- const filePath = parts[1]!.trim();
209
- if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
210
- seen.add(filePath);
211
- changes.push({ status: status as "M" | "A" | "D", path: filePath });
212
- }
213
- }
214
- ```
215
-
216
- **3b. 修改 `git status --porcelain` 的解析。**
217
-
218
- 同理,`git status --porcelain` 的输出格式为 `XY filepath\n`(如 ` M .gitignore`、`?? newfile.ts`、`A filepath`)。可以用字符串拆分:
219
-
220
- ```typescript
221
- // git status --porcelain 格式:XY filepath (e.g., " M .gitignore", "?? newfile.ts")
222
- // 前两个字符是状态,后面的空格分隔,然后是文件路径
223
- const statusPrefix = trimmed.slice(0, 2); // e.g., "??", " M", "A "
224
- const filePath = trimmed.slice(3).trim(); // after "?? " or " M "
225
- if (filePath && !seen.has(filePath) && (statusPrefix === "??" || statusPrefix === "A " || statusPrefix.startsWith("A"))) {
226
- seen.add(filePath);
227
- changes.push({ status: "A", path: filePath });
228
- }
229
- ```
230
-
231
- **注意**:`git status --porcelain` 的前两个字符格式是固定的 `XY`(X 是 index 状态,Y 是 working tree 状态)。`??` 表示 untracked,`A ` 表示 staged new,` M` 表示 modified in working tree 等。
232
-
233
- 但当前代码只关心 "??" 和 "A "(以 "A" 开头),所以直接检查前两个字符即可。
234
-
235
- - **验证方式**:运行 `node tests/test-loopcount-timeout-fix.mjs`
236
-
237
- ## 依赖关系
238
-
239
- - 步骤 1、2 修改同一文件(`ui-helpers.ts`),但改动在不同位置,可独立实施
240
- - 步骤 3 修改不同文件(`workflow-engine.ts`),完全独立于步骤 1、2
241
- - 三个步骤可以按任意顺序实施,互不依赖
242
-
243
- ## 测试策略
244
-
245
- - 运行现有测试 `node tests/test-loopcount-timeout-fix.mjs`,确保所有通过
246
- - 手动审查相关代码路径,确认改动正确
247
-
248
- ## 注意事项
249
-
250
- - **超时时间在子代理行的显示**:当前的 `sub.detail` 已在 `runAgentWithProgress` 中设置为 `超时时间60m`(具体为 `` 超时时间${formatTimeout(timeoutMs)} ``)。但要注意,`sub.detail` 的显示位置需要调整——当前逻辑是在 `childItems` 为空时才显示 `sub.detail`,作为子项。需要改为直接在 agent 行末尾显示 `(当前计时/${sub.detail})`。同时需要注意:当 sub-step 有 tools/outputs 时,`sub.detail` 不应再作为子项出现(避免重复)。
251
- - **show detail change**: 当前 `sub.detail` 只在 `childItems.length === 0` 时显示为子项。修改后,`sub.detail` 应被解析为超时信息拼接到 agent 行末尾,而不作为独立的子项。因此需要修改子代理行的渲染逻辑。
252
- - **子代理超时时间的 data flow**: 当前 `runAgentWithProgress` 已在 sub-step 的 `detail` 字段写入 `超时时间60m`。这个 detail 可以直接重用。但需要区分:`detail` 目前既被当做"超时时间文本"使用,也被当做"其他详情文本"(当没有 tools/outputs 时)。修改后,如果 `detail` 包含 "超时时间",则应解析到 agent 行;否则仍作为普通子项。
253
- - 更简单的做法是:**直接在内置渲染中为 sub-step 添加超时时间字段**,或者将超时时间单独存为一个字段。但为了最小改动,直接利用现有的 `detail` 字段(已在 `runAgentWithProgress` 中设置为 `超时时间60m`),在 agent 行渲染时拼接。
@@ -1,137 +0,0 @@
1
- # [fix] Esc 双击确认停止工作流 — 实施计划
2
-
3
- ## 概述
4
-
5
- 修复工作流运行期间,一次 Esc 键就立即中断工作流的问题。改为:
6
- - 第一次按 Esc:显示提示 "再次按下 Esc 键,停止 Workflow"
7
- - 两次 Esc 间隔 < 5s 才退出
8
- - 超过 5s 后重置状态,重新监听
9
-
10
- **改动范围极小**,只修改 `workflow-engine.ts` 中 `runWorkflow` 函数内的 `onTerminalInput` 回调逻辑。
11
-
12
- ## 文件清单
13
-
14
- ### 修改文件
15
- | 文件路径 | 改动描述 | 风险等级 |
16
- |---------|---------|---------|
17
- | `extensions/workflow-engine.ts` | Esc 处理逻辑:增加二次确认、5s 时间窗口、提示显示 | 低 |
18
-
19
- ### 新增文件
20
- 无
21
-
22
- ### 删除文件
23
- 无
24
-
25
- ## 实施步骤
26
-
27
- ### 步骤 1:修改 Esc 处理逻辑(二次确认 + 5s 时间窗口)
28
-
29
- - **前置条件**:无
30
- - **改动文件**:`extensions/workflow-engine.ts`
31
- - **改动内容**:
32
-
33
- 定位到 `runWorkflow` 函数末尾的 `onTerminalInput` 回调(当前第 1709-1718 行):
34
-
35
- ```typescript
36
- // ── Register terminal input handler (Esc to cancel) ──
37
- if (ctx.hasUI) {
38
- _terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
39
- if (!matchesKey(data, Key.escape)) return undefined;
40
- if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
41
- ctx.ui.notify("⏹️ 用户取消工作流", "warning");
42
- cancelWorkflow();
43
- return { consume: true };
44
- }
45
- return undefined;
46
- });
47
- }
48
- ```
49
-
50
- **改为**(关键改动):
51
-
52
- ```typescript
53
- // ── Register terminal input handler (Esc to cancel, with double-press confirmation) ──
54
- if (ctx.hasUI) {
55
- let _lastEscPressTime = 0;
56
- _terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
57
- if (!matchesKey(data, Key.escape)) return undefined;
58
- if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
59
- const now = Date.now();
60
- if (_lastEscPressTime > 0 && now - _lastEscPressTime < 5000) {
61
- // Second Esc press within 5s → confirm cancel
62
- ctx.ui.notify("⏹️ 正在停止工作流...", "warning");
63
- cancelWorkflow();
64
- _lastEscPressTime = 0;
65
- return { consume: true };
66
- }
67
- // First Esc press (or expired) → show hint
68
- _lastEscPressTime = now;
69
- ctx.ui.notify("再次按下 Esc 键,停止 Workflow", "warning");
70
- return { consume: true };
71
- }
72
- return undefined;
73
- });
74
- }
75
- ```
76
-
77
- - **改动说明**:
78
- 1. 引入 `_lastEscPressTime` 变量(函数块作用域),记录上一次 Esc 按下的时间
79
- 2. 第一次按 Esc → 记录时间并显示提示 "再次按下 Esc 键,停止 Workflow"
80
- 3. 在 5s 内再次按 Esc → 执行取消操作
81
- 4. 超过 5s 后按 Esc → 重置为第一次状态(因为 `_lastEscPressTime > 0` 但差值 >= 5000,被视为过期,走到 `_lastEscPressTime = now` 分支重新计时)
82
-
83
- - **验证方式**:
84
- 1. 手动测试:启动工作流,按一次 Esc → 应看到提示,工作流继续
85
- 2. 手动测试:5s 内再按一次 Esc → 工作流取消
86
- 3. 手动测试:按一次 Esc,等待 5s+,再按一次 Esc → 相当于第一次,显示提示
87
- 4. 确保原有取消功能在二次确认后正常运作(清理 widget、保存 checkpoint、归档)
88
- 5. 确保 Esc 在其他非工作流场景的行为不受影响(只修改了 `_workflowRunning` 为 true 时的分支)
89
-
90
- ### 步骤 2:清理 `_lastEscPressTime` 状态
91
-
92
- - **前置条件**:步骤 1 完成
93
- - **改动文件**:`extensions/workflow-engine.ts`
94
- - **改动内容**:
95
-
96
- 在 `cleanupWidget()` 函数中,确保 `_lastEscPressTime` 在 cleanup 时会被自然重置。但由于 `_lastEscPressTime` 是 `onTerminalInput` 回调闭包内的局部变量,当 `_terminalInputUnsubscribe()` 被调用时,闭包和 `_lastEscPressTime` 都会自然被 GC 回收。
97
-
98
- **不需要额外改动**——`cleanupWidget()` 中已有的逻辑:
99
- ```typescript
100
- if (_terminalInputUnsubscribe) {
101
- _terminalInputUnsubscribe();
102
- _terminalInputUnsubscribe = null;
103
- }
104
- ```
105
- 已经在工作流结束时正确地解除了监听器注册。下次 `runWorkflow` 被调用时,会新建一个闭包和新的 `_lastEscPressTime` 变量。
106
-
107
- - **验证方式**:
108
- 1. 运行工作流,按 Esc 两次确认取消
109
- 2. 确认所有 cleanup 逻辑正确执行
110
- 3. 启动新的工作流,测试 Esc 逻辑从头开始工作
111
-
112
- ## 依赖关系
113
-
114
- - 步骤 2 是验证步骤,不涉及代码改动
115
- - 仅需修改一个函数回调中的逻辑
116
-
117
- ## 测试策略
118
-
119
- 1. **人工测试(主要方式)**:
120
- - 启动一个工作流(如 `/dev-feat` → 快速链式)
121
- - 按 Esc → 验证显示提示 "再次按下 Esc 键,停止 Workflow"
122
- - 工作流继续正常运行
123
- - 5s 内再按 Esc → 工作流取消,widget 消失
124
- - 重新启动工作流,按 Esc,等 5s+,再按 Esc → 显示提示(重置为第一次)
125
-
126
- 2. **单元测试** :
127
- - 因 `onTerminalInput` 回调直接依赖 `ctx.ui` 和终端环境,不方便做纯单元测试
128
- - 可考虑在 `tests/` 目录下新增测试文件对回调逻辑进行隔离测试(mock `matchesKey`、`_workflowRunning` 等)
129
-
130
- ## 注意事项
131
-
132
- - **最小改动原则**:只修改 `runWorkflow` 内的 `onTerminalInput` 回调,约 15 行代码
133
- - **性能**:无影响,仅增加一个 `Date.now()` 调用和简单比较
134
- - **并发安全**:`_workflowRunning` 是整个模块级别的标志,`_lastEscPressTime` 是闭包局部变量,不存在竞态问题
135
- - **不要影响其他 Esc 处理**:其他地方的 Esc 处理(如 `uiSelect`、`uiConfirm` 中的 `onEscape` 回调)完全不受影响,它们由不同的 TUI 组件管理
136
- - **不要破坏工作流的正常运行**:第一次 Esc 只是显示提示、不执行任何取消操作,工作流步骤继续执行
137
- - **不要改变 notify 行为**:使用已有的 `ctx.ui.notify()` API,与代码库中其他通知一致
@@ -1,258 +0,0 @@
1
- # 修复 git diff 解析与循环计数 Bug — 实施计划
2
-
3
- ## 概述
4
-
5
- 修复两个 Bug:
6
- 1. **git diff 解析**:`f98799d` commit 中将 `getGitDiffChanges()` 的 git diff 解析从正则改为简单的 `split("\t")`,导致无法正确处理非 tab 分隔的输出(如 space-padded 格式),且 agent 输出文本被错误解析为文件路径(如 `checkpoint-${planId}.json`、`[],\t\t\toutputs:`)。
7
- 2. **循环计数偏移**:`executeLoopGroup` 中 `loopCount++` 在 reviewer 完成后才执行,导致第二次循环开始时 widget 仍显示旧的 loopCount,造成 "第 1 次循环" 重复出现。
8
-
9
- ## 文件清单
10
-
11
- ### 修改文件
12
- | 文件路径 | 改动描述 | 风险等级 |
13
- |---------|---------|---------|
14
- | `extensions/workflow-engine.ts` | 修复 `getGitDiffChanges` 解析逻辑;修复 `loopCount` 更新时机 | 中 |
15
- | `extensions/ui-helpers.ts` | 修复 `buildWidgetLines` 中循环计数的 fallback 逻辑 | 低 |
16
-
17
- ## 实施步骤
18
-
19
- ---
20
-
21
- ### 步骤 1:修复 `getGitDiffChanges` 中 git diff 输出的解析
22
-
23
- - **前置条件**:无
24
- - **改动文件**:`extensions/workflow-engine.ts`(函数 `getGitDiffChanges`)
25
- - **改动内容**:
26
-
27
- **问题**:`f98799d` 将原来健壮的正则解析 `/^([MAD])\s+(.+)$/` 改为简单的 `split("\t")`。Git 的 `--name-status` 输出格式在不同环境/版本中可能使用 space-padded 格式(如 `"M path/to/file"`),此时 `split("\t")` 只能得到 `parts.length === 1`,导致解析失败。结果文件变更不会被检测到。
28
-
29
- **修复方案**:将解析改回使用正则 `^([MAD])\s+(.+)$`,同时保留 tab split 作为后备(兼容两种格式):
30
-
31
- ```typescript
32
- // 原来的正则方式(健壮):
33
- // git diff --name-status output format: X\tfilepath or "X filepath"
34
- const statusMatch = trimmed.match(/^([MAD])\s+(.+)$/);
35
- if (statusMatch) {
36
- const status = statusMatch[1]!.trim();
37
- const filePath = statusMatch[2]!.trim();
38
- if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
39
- seen.add(filePath);
40
- changes.push({ status: status as "M" | "A" | "D", path: filePath });
41
- }
42
- }
43
- // 后备:tab split(兼容部分 git 版本输出的 tab 格式)
44
- else if (trimmed.includes("\t")) {
45
- const parts = trimmed.split("\t");
46
- if (parts.length === 2) {
47
- const status = parts[0]!.trim();
48
- const filePath = parts[1]!.trim();
49
- if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
50
- seen.add(filePath);
51
- changes.push({ status: status as "M" | "A" | "D", path: filePath });
52
- }
53
- }
54
- }
55
- ```
56
-
57
- 同时修复 `git status --porcelain` 部分:当前代码用 `trimmed.slice(0, 2)` 和 `trimmed.slice(3)`,但 `--porcelain` 格式是固定的 2 字符状态码 + 1空格 + path。需要更健壮的处理:
58
-
59
- ```typescript
60
- // 原来:statusPrefix = trimmed.slice(0, 2); filePath = trimmed.slice(3).trim();
61
- // 改为正则(更健壮):
62
- const statusMatch2 = trimmed.match(/^(..)\s+(.+)$/);
63
- if (statusMatch2) {
64
- const statusPrefix = statusMatch2[1]!.trim();
65
- const filePath = statusMatch2[2]!.trim();
66
- if (filePath && !seen.has(filePath) && (statusPrefix === "??" || statusPrefix === "A " || statusPrefix.startsWith("A"))) {
67
- seen.add(filePath);
68
- changes.push({ status: "A", path: filePath });
69
- }
70
- }
71
- ```
72
-
73
- - **验证方式**:手动执行 `git diff --name-status` 验证输出格式,确认正则能够正确解析。
74
-
75
- ---
76
-
77
- ### 步骤 2:修复 agent 输出文本刮取逻辑中的脏数据
78
-
79
- - **前置条件**:无
80
- - **改动文件**:`extensions/workflow-engine.ts`(函数 `runAgentWithProgress` 中的文本刮取部分)
81
- - **改动内容**:
82
-
83
- **问题**:五个 `filePatterns` 正则过于宽松,会从 agent 的自然语言输出中误匹配脏数据。具体来说:
84
-
85
- 1. Pattern `/(?:^|\n)\s*(?:edit|new|delete|read|modify|create|update|add|remove)\s*[::]\s*([^\n]+\.[a-zA-Z0-9_]+)/gim` 可以匹配 agent 文本中类似:
86
- - "M [],\n\t\t\t\toutputs:"(遇到 "M" 不匹配,但 "remove"或其他匹配?不,这个 pattern 需要前面的动词)
87
- - 实际上,agent 的输出中可能有类似这样的文本:
88
- ```
89
- modify: [],\n\t\t\t\toutputs: ...
90
- ```
91
- 或进度消息中的其他文本片段。
92
-
93
- 2. 标记代码块的 pattern `` /`([^`]+\.[a-zA-Z0-9_]+)`/g `` 可能匹配到 `` `checkpoint-xxx.json` `` 或 `` `checkpoint-${planId}.json` `` 这样的模板字符串。
94
-
95
- **修复方案**:在 `filePatterns` 的每个匹配结果后添加更严格的过滤器,排除明显不是文件路径的字符串:
96
-
97
- ```typescript
98
- // 在 filePath 验证后添加额外过滤
99
- // 过滤器:排除包含不合法路径字符或模板表达式的字符串
100
- if (filePath.includes("${") || filePath.includes("\\n") || filePath.includes("\\t")) continue; // 排除模板字符串和转义字符
101
- if (filePath.includes("[]") || filePath.includes("{}")) continue; // 排除数组/对象字面量
102
- if (filePath.match(/^[\s,;)\]}]+$/)) continue; // 排除纯符号
103
- ```
104
-
105
- 关键修改位置:`runAgentWithProgress` 函数中,`const filePath = m[1]!.trim()` 之后的验证逻辑块。
106
-
107
- - **验证方式**:用包含 `checkpoint-\${planId}.json` 和 `[],\n\t\t\t\toutputs:` 等脏数据的测试文本运行逻辑,确认不会产生误匹配。
108
-
109
- ---
110
-
111
- ### 步骤 3:修复循环计数偏移
112
-
113
- - **前置条件**:步骤 1 和 2 完成
114
- - **改动文件**:`extensions/workflow-engine.ts`(函数 `executeLoopGroup`)和 `extensions/ui-helpers.ts`(函数 `buildWidgetLines`)
115
- - **改动内容**:
116
-
117
- **根本原因**:`executeLoopGroup` 中的循环计数更新顺序有误。当前的顺序是:
118
-
119
- 1. 进入 while 循环(此时 `loopCount` 还未递增)
120
- 2. 重置 sub-step 为 pending
121
- 3. 执行 worker agent
122
- 4. 执行 reviewer agent
123
- 5. `loopCount++` 并 `state.loopCount = loopCount`
124
- 6. 检查是否需要继续循环
125
-
126
- 当 reviewer 发现 critical 问题需要再次循环时,`loopCount` 已经在步骤 5 增加为 1,所以在第二次循环开始时 widget 显示的是 "第 1 次循环"(因为 `state.loopCount = 1`),但实际上用户期望看到的是 "第 2 次循环"(即将开始第 2 轮)。
127
-
128
- 更准确地说,期望的显示行为是:
129
- - Pending 时:`第 0 次循环`(表示尚未开始)
130
- - 第 1 次循环执行中:`第 1 次循环`
131
- - 第 1 次循环完成,需要第 2 次循环,第二次循环执行中:`第 2 次循环`
132
- - ...
133
-
134
- **修复方案 A(推荐,最小改动)**:在 while 循环**开始处**(进入新的一轮循环之前)更新 loopCount。
135
-
136
- 将 `loopCount++` 和 `state.loopCount = loopCount` 从 reviewer 完成之后**移到 while 循环最开头**。这样:
137
-
138
- ```typescript
139
- while (loopCount < maxLoops) {
140
- loopCount++; // 递增计数,表示"即将开始第 N 次循环"
141
- state.loopCount = loopCount;
142
-
143
- // 立即更新 UI
144
- updateWidgetStep(stepIndex, step.label, "running", {
145
- loopCount,
146
- maxLoops: step.maxLoops,
147
- startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
148
- });
149
-
150
- // 重置 sub-step 状态
151
- setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
152
- setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
153
- // ... 后续逻辑 ...
154
- }
155
- ```
156
-
157
- 同时需要移除原来 reviewer 完成后的 `loopCount++` 和 `state.loopCount = loopCount` 部分(在 `if (reviewSummary?.maxSeverity === "critical" ...)` 判断之前)。
158
-
159
- **注意**:由于 `loopCount` 现在从 1 开始递增(而不是原来的从 0 开始,在 reviewer 完成后才 ++),所以需要同步修改 `buildWidgetLines` 中的 fallback 逻辑:
160
-
161
- ```typescript
162
- // 在 ui-helpers.ts 的 buildWidgetLines 中:
163
- if (s.maxLoops != null) {
164
- if (isRunning) {
165
- // 当 loop-group 开始运行时,loopCount 已经通过 executeLoopGroup 在循环开头设置了,
166
- // 所以不需要 fallback 显示"第 1 次循环"
167
- // 直接使用 s.loopCount 的值
168
- if (s.loopCount == null || s.loopCount === 0) {
169
- // 安全 fallback(理论上不会走到这里)
170
- loopStr = dim(theme, ` · 第 1 次循环`);
171
- }
172
- } else if (isPending) {
173
- loopStr = dim(theme, ` · 第 0 次循环`);
174
- }
175
- }
176
- ```
177
-
178
- - **验证方式**:
179
- 1. 启动工作流,观察 loop-group 的循环计数显示
180
- 2. 验证第 1 次循环显示 `第 1 次循环`
181
- 3. 当 reviewer 触发再次循环时,验证显示 `第 2 次循环` 而不是 `第 1 次循环`
182
- 4. 验证第 3 次循环显示 `第 3 次循环`
183
-
184
- ---
185
-
186
- ### 步骤 4:同步修改 loadCheckpoint 恢复时的循环计数
187
-
188
- - **前置条件**:步骤 3 完成
189
- - **改动文件**:`extensions/workflow-engine.ts`(`runWorkflow` 函数中恢复 checkpoint 的逻辑)
190
- - **改动内容**:
191
-
192
- 由于 `loopCount` 的语义发生变化(从"已完成次数"变为"当前正在进行的轮次"),需要确保从 checkpoint 恢复时,`loopCount` 能正确恢复。
193
-
194
- 当前 checkpoint 中的 `loopCounts[step.id]` 存储的是已完成次数(即原来的语义)。如果 loopCount 现在从 1 开始,则恢复时需要确保:
195
-
196
- - 如果 checkpoint 中 `loopCounts[step.id] = 1`(已完成 1 次),恢复后应显示 `第 1 次循环` 但不重新执行已完成的工作。
197
-
198
- 但检查代码逻辑:checkpoint 恢复时会跳过 `status === "done"` 的步骤,所以 `loopCounts` 只对**未完成**的 loop-group 步骤有效。对于未完成的步骤,`loopCounts` 为 0 或上一次退出时的值。
199
-
200
- **实际上不需要修改**,因为 `loopCounts[step.id]` 只作为 `while` 循环的起始值(`let loopCount = loopCounts[step.id] ?? 0;`)。在步骤 3 中,我们将 `loopCount++` 移到了 while 开头,所以:
201
-
202
- - 恢复后 loopCount = previous_loopCount(已完成次数)
203
- - while 开始执行时立即 ++,变成 previous_loopCount + 1(当前轮次)
204
-
205
- 这恰好是正确的行为。
206
-
207
- - **验证方式**:从 checkpoint 恢复工作流,确认循环计数正确。
208
-
209
- ---
210
-
211
- ## 依赖关系
212
-
213
- - 步骤 1 和 2 相互独立,可并行实施
214
- - 步骤 3 独立于步骤 1、2
215
- - 步骤 4 依赖步骤 3
216
-
217
- ## 测试策略
218
-
219
- ### 单元测试(手动验证)
220
-
221
- 1. **git diff 解析测试**:
222
- - 用以下格式模拟 git diff 输出:
223
- - `M\tpath/to/file.ts`(tab 分隔)
224
- - `M path/to/file.ts`(空格分隔,多空格)
225
- - `A\tnewfile.ts`
226
- - `D\tdeletedfile.ts`
227
- - 验证所有格式都能正确解析
228
-
229
- 2. **文本刮取过滤器测试**:
230
- - 用以下文本测试 `filePatterns` 匹配:
231
- - `checkpoint-${planId}.json`
232
- - `[],\n\t\t\t\toutputs:`
233
- - `edit: src/main.rs`(应匹配)
234
- - `I've modified src/main.rs`(应匹配)
235
- - `try`(不应匹配)
236
- - 验证过滤器正确排除脏数据
237
-
238
- 3. **循环计数测试**:
239
- - 模拟 loop-group 执行流程:
240
- ```
241
- pending → 第 0 次循环
242
- running 第一次循环 → 第 1 次循环
243
- running 第二次循环 → 第 2 次循环
244
- running 第三次循环 → 第 3 次循环
245
- ```
246
- - 验证显示值正确
247
-
248
- ### 集成测试
249
-
250
- 1. 运行一个实际工作流,观察 UI 显示
251
- 2. 手动触发 reviewer 发现 bug,观察再次循环时的显示
252
-
253
- ## 注意事项
254
-
255
- 1. **最小改动原则**:只修改有 bug 的逻辑,不重构其他部分
256
- 2. **向后兼容**:checkpoint 文件格式不变,`loopCounts` 字段语义变化需要确保从旧 checkpoint 恢复时行为正确
257
- 3. **git diff 解析**:改回正则解析的同时保留 tab split 后备,兼容多种 git 输出格式
258
- 4. **文本刮取**:添加的过滤器不应影响正常文件路径的匹配(如 `src/main.rs`、`extensions/workflow-engine.ts` 等)