@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,150 +0,0 @@
1
- # 修复 workflow-engine 中两个 Bug — 实施计划
2
-
3
- ## 概述
4
-
5
- 修复 `extensions/workflow-engine.ts` 中的两个 Bug:
6
-
7
- 1. **Bug A — executeLoopGroup 缺少 exitCode 检查**(第 1179 行):`executeLoopGroup` 函数在调用 `runAgentWithProgress` 后,只处理了超时(`isTimeoutResult`),但没有检查 sub-agent 非正常退出(exitCode !== 0 且 exitCode !== -1)的情况。对比 `executeSingleStep` 在第 1146 行有显式的 exitCode 检查。这会导致 agent 崩溃或报错时,工作流继续运行 reviewer,产生错误的结果。
8
-
9
- 2. **Bug B — setTimeout cleanupWidget 竞态条件**(第 1436 行和第 1648 行):工作流完成或取消后,使用 `setTimeout(() => cleanupWidget(), 5000)` 延迟 5 秒清理 widget。如果用户在这 5 秒内启动新工作流,定时器触发时会调用 `cleanupWidget`,将 `_workflowRunning` 设为 `false` 并清空 `_lastWorkflowCtx`,破坏正在运行的新工作流。
10
-
11
- ## 根因分析
12
-
13
- **Bug A 根因**:`executeLoopGroup` 在 2025 年 3 月的迭代中从 `executeSingleStep` 分支出来,当时只实现了超时处理逻辑(`isTimeoutResult`),但遗漏了通用的 exitCode 非零检查。`executeSingleStep` 在第 1146 行有 `if (result.exitCode !== 0 && result.stderr) { throw new Error(...); }`,但 `executeLoopGroup` 中没有对应逻辑。
14
-
15
- **Bug B 根因**:使用延迟 `setTimeout` 进行异步清理是一种脆弱的模式。它假设在定时器超时前不会有新的工作流启动,但用户可能在完成消息查看后立即开始新的工作流。`cleanupWidget` 会无条件重置 `_workflowRunning` 和 `_lastWorkflowCtx` 等全局状态,没有任何保护机制。
16
-
17
- ## 文件清单
18
-
19
- ### 修改文件
20
- | 文件路径 | 改动描述 | 风险等级 |
21
- |---------|---------|---------|
22
- | `extensions/workflow-engine.ts` | 修复 Bug A(添加 exitCode 检查)和 Bug B(定时器竞态保护) | 低 |
23
-
24
- ### 新增文件
25
- | 文件路径 | 用途说明 |
26
- |---------|---------|
27
- | `tests/test-workflow-engine-bugs.mjs` | 复现并验证 Bug A 和 Bug B 的修复 |
28
-
29
- ## 实施步骤
30
-
31
- ### 步骤 1:修复 executeLoopGroup 缺少 exitCode 检查(Bug A)
32
-
33
- - **前置条件**:无
34
- - **改动文件**:`extensions/workflow-engine.ts`
35
- - **改动位置**:第 1179 行 `let agentResult = await runAgentWithProgress(...)` 之后
36
- - **改动内容**:在 `isTimeoutResult(agentResult)` 判断之前,插入 exitCode 检查。如果 `agentResult.exitCode !== 0` 且 `agentResult.exitCode !== -1`(-1 是超时标记),则根据 mode 分支处理:
37
- - **full-auto 模式**:直接 `throw new Error(...)`,由上层 `executeWorkflowBackground` 的 catch 块捕获,将步骤标记为 failed。
38
- - **非 full-auto 模式**:弹出 UI 选择,让用户选择"重新执行"、"跳过此步骤"或"取消工作流"(与超时处理的分支逻辑一致)。
39
-
40
- 具体代码片段(在 `isTimeoutResult` 检查之前插入):
41
-
42
- ```typescript
43
- // 检查 agent 是否异常退出(非超时非零退出码)
44
- if (result.exitCode !== 0 && !isTimeoutResult(result)) {
45
- if (mode === "full-auto") {
46
- throw new Error(`Agent ${step.loopAgentName} 异常退出 (exit ${result.exitCode}): ${result.stderr.slice(0, 200)}`);
47
- } else {
48
- const choice = await uiSelect(ctx, `❌ ${step.loopAgentName} 异常退出 (exit ${result.exitCode})`, [
49
- "1. 重新执行", "2. 跳过此步骤", "3. 取消工作流",
50
- ]);
51
- if (!choice || choice.startsWith("3")) { cancelWorkflow(); return; }
52
- if (choice.startsWith("2")) { state.status = "skipped"; return; }
53
- // 重新执行
54
- result = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, stepIndex, step.loopAgentName!, step.timeoutMs);
55
- }
56
- }
57
- ```
58
-
59
- - **验证方式**:运行 `node tests/test-workflow-engine-bugs.mjs` 确认测试通过
60
-
61
- ### 步骤 2:修复 setTimeout cleanupWidget 竞态条件(Bug B)
62
-
63
- - **前置条件**:步骤 1 完成
64
- - **改动文件**:`extensions/workflow-engine.ts`
65
- - **改动位置**:
66
- 1. 第 1436 行:`executeWorkflowBackground` 函数末尾的 `setTimeout(() => cleanupWidget(), 5000);`
67
- 2. 第 1648 行:`cancelWorkflow` 回调中的 `setTimeout(() => cleanupWidget(), 5000);`
68
- - **改动内容**:引入一个模块级别的定时器 ID 变量 `_cleanupTimer: ReturnType<typeof setTimeout> | null`,并在以下两个位置修改:
69
-
70
- 1. 声明新变量(在全局变量区域,约第 606 行附近):
71
- ```typescript
72
- let _cleanupTimer: ReturnType<typeof setTimeout> | null = null;
73
- ```
74
-
75
- 2. 修改第 1436 行的 `setTimeout`:
76
- ```typescript
77
- // 清除之前的定时器
78
- if (_cleanupTimer) clearTimeout(_cleanupTimer);
79
- _cleanupTimer = setTimeout(() => {
80
- _cleanupTimer = null;
81
- cleanupWidget();
82
- }, 5000);
83
- ```
84
-
85
- 3. 修改第 1648 行的 `setTimeout`:
86
- ```typescript
87
- if (_cleanupTimer) clearTimeout(_cleanupTimer);
88
- _cleanupTimer = setTimeout(() => {
89
- _cleanupTimer = null;
90
- cleanupWidget();
91
- }, 5000);
92
- ```
93
-
94
- 4. 在 `initWidget` 函数中(约第 639 行)添加清除逻辑,确保新工作流启动时取消旧定时器:
95
- ```typescript
96
- if (_cleanupTimer) {
97
- clearTimeout(_cleanupTimer);
98
- _cleanupTimer = null;
99
- }
100
- ```
101
-
102
- 5. 在 `cleanupWidget` 函数中(约第 790 行)添加清除逻辑:
103
- ```typescript
104
- if (_cleanupTimer) {
105
- clearTimeout(_cleanupTimer);
106
- _cleanupTimer = null;
107
- }
108
- ```
109
-
110
- - **验证方式**:运行 `node tests/test-workflow-engine-bugs.mjs` 确认测试通过
111
-
112
- ### 步骤 3:编写测试用例
113
-
114
- - **前置条件**:步骤 1 和步骤 2 完成
115
- - **新增文件**:`tests/test-workflow-engine-bugs.mjs`
116
- - **测试内容**:
117
-
118
- **Bug A 测试**:
119
- - **测试 1**:模拟 `SubagentResult` 对象,验证 `executeLoopGroup` 在收到 `exitCode: 1` 且 `stderr: "some error"` 时的行为
120
- - 构造 `{ exitCode: 1, stderr: "Agent crashed: OOM", output: "" }`
121
- - 验证 `isTimeoutResult` 返回 `false`
122
- - 验证自定义的 simulate 函数能正确识别非零退出码
123
- - **测试 2**:验证 `executeSingleStep` 已有 exitCode 检查(确认现有行为不被破坏)
124
- - **测试 3**:验证 `isTimeoutResult` 对 `{ exitCode: -1, stderr: "timed out" }` 返回 `true`(确认超时仍被正确识别)
125
-
126
- **Bug B 测试**:
127
- - **测试 4**:模拟定时器竞态场景
128
- - 验证 `initWidget` 被调用时能清除旧的 `_cleanupTimer`
129
- - 验证 `cleanupWidget` 被调用时能清除 `_cleanupTimer`
130
- - 验证新工作流启动后,旧定时器不会触发
131
-
132
- - **验证方式**:运行 `node tests/test-workflow-engine-bugs.mjs`
133
-
134
- ## 依赖关系
135
-
136
- - 步骤 1 和步骤 2 相互独立,可并行实施
137
- - 步骤 3 依赖步骤 1 和步骤 2 完成
138
-
139
- ## 测试策略
140
-
141
- - **Bug A 单元测试**:通过模拟 `SubagentResult` 对象和 `isTimeoutResult` 函数,验证非零退出码被正确识别和处理
142
- - **Bug B 单元测试**:通过模拟定时器 ID 管理和 `initWidget` 的清理行为,验证竞态条件被消除
143
- - **回归测试**:运行现有测试 `node tests/test-workflow-engine.mjs` 确认无破坏
144
-
145
- ## 注意事项
146
-
147
- 1. **最小化改动**:只插入必要的新逻辑,不重构现有代码结构
148
- 2. **与 executeSingleStep 保持一致**:Bug A 的修复逻辑应与 `executeSingleStep` 第 1146 行的 exitCode 检查保持一致
149
- 3. **定时器清除顺序**:在 `initWidget` 中清除旧定时器必须在设置 `_workflowRunning = true` **之前**完成,确保不会在旧定时器触发和新定时器设置之间出现窗口期
150
- 4. **手动确认**:部署后需手动测试快速连续启动两个工作流的场景
@@ -1,215 +0,0 @@
1
- # 修复 Workflow loopCount UI 不同步 & 超时时间分离 — 实施计划
2
-
3
- ## 概述
4
-
5
- 修复 workflow-engine.ts 中三个相互关联的 Bug:
6
-
7
- 1. **loopCount UI 不同步**:`executeLoopGroup()` 在 while 循环中每次迭代后 `loopCount++`,但未调用 `updateWidgetStep()` 更新 widget 状态,导致 widget 中的 `loopStr` 在第 2、3、4 次循环时仍显示"第 1 次循环"。
8
- 2. **reviewer 与 loopAgent 共用超时**:`executeLoopGroup()` 中 reviewer 调用 `runAgentWithProgress()` 时传的是 `step.timeoutMs`,未使用独立超时时间。
9
- 3. **默认超时值不准确**:worker 应为 30min、trimmer 应为 20min、reviewer 应为 15min。
10
-
11
- ## 根因分析
12
-
13
- ### Bug 1: loopCount UI 不同步
14
- - `executeLoopGroup()` (workflow-engine.ts L1197-1270) 中,`loopCount++` (L1243) 之后没有任何 `updateWidgetStep()` 调用。
15
- - 外层的 `executeWorkflowBackground()` 只在步骤完成时(done/failed)调用 `updateWidgetStep()` 更新 `loopCount`,因此中间迭代的 loopCount 从未通知 widget。
16
- - Widget 端 `buildWidgetLines()` (ui-helpers.ts L460-470) 中,`loopStr` 显示逻辑依赖于 `s.loopCount`,且当 `isRunning && s.loopCount == null` 时硬编码显示"第 1 次循环"——这进一步掩盖了问题。
17
-
18
- ### Bug 2: reviewer 超时未分离
19
- - `WorkflowStepDef` 接口没有 `reviewTimeoutMs` 字段。
20
- - `executeLoopGroup()` L1224 行:`runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, step.timeoutMs)` — reviewer 和 loopAgent 使用同一个 `step.timeoutMs`。
21
-
22
- ### Bug 3: 默认超时值不正确
23
- - `dev-prompts.ts` 中所有 `worker` 相关的 loop-group 的 `timeoutMs` 都是 `900_000` (15min),应为 `1_800_000` (30min)。
24
- - `trimmer` 相关的 loop-group 的 `timeoutMs` 是 `300_000` (5min),应为 `1_200_000` (20min)。
25
- - reviewer 的独立超时(通过 `reviewTimeoutMs`)应为 `900_000` (15min)。
26
-
27
- ## 文件清单
28
-
29
- ### 修改文件
30
- | 文件路径 | 改动描述 | 风险等级 |
31
- |---------|---------|---------|
32
- | `extensions/workflow-engine.ts` | 1) 给 `WorkflowStepDef` 增加 `reviewTimeoutMs` 字段;2) `executeLoopGroup()` 中 loopCount++ 后调用 `updateWidgetStep()`;3) reviewer 调用 `runAgentWithProgress()` 使用 `reviewTimeoutMs`;4) loop-group 行不显示 timeout | 中 |
33
- | `extensions/dev-prompts.ts` | 1) 修改所有 loop-group 的 `timeoutMs` 默认值;2) 为所有 loop-group 添加 `reviewTimeoutMs` 字段 | 低 |
34
- | `extensions/ui-helpers.ts` | 1) `buildWidgetLines()` 中 loop-group 行不显示超时时间;2) sub-step 级别显示各自的超时时间 | 低 |
35
-
36
- ### 新增文件
37
- | 文件路径 | 用途说明 |
38
- |---------|---------|
39
- | `tests/test-loopcount-timeout-fix.mjs` | 测试 loopCount 同步、reviewer 独立超时、默认值修改 |
40
-
41
- ## 实施步骤
42
-
43
- ### 步骤 1:修改 `WorkflowStepDef` 接口,增加 `reviewTimeoutMs` 字段
44
- - **前置条件**:无
45
- - **改动文件**:`extensions/workflow-engine.ts`
46
- - **改动内容**:在 `WorkflowStepDef` 接口中增加可选字段 `reviewTimeoutMs?: number;`(第 49-58 行)
47
- - **验证方式**:TypeScript 编译不报错
48
-
49
- ### 步骤 2:修改 `dev-prompts.ts` 中所有 loop-group 的默认超时值
50
- - **前置条件**:步骤 1 完成
51
- - **改动文件**:`extensions/dev-prompts.ts`
52
- - **改动内容**:
53
-
54
- 1. **FEAT_WORKFLOW_STEPS** (第 369-401 行)
55
- - `worker-reviewer` 的 `timeoutMs`: `900_000` → `1_800_000`,新增 `reviewTimeoutMs: 900_000`
56
- - `trimmer-reviewer` 的 `timeoutMs`: `300_000` → `1_200_000`,新增 `reviewTimeoutMs: 900_000`
57
-
58
- 2. **FIX_WORKFLOW_STEPS** (第 404-428 行)
59
- - `worker-reviewer` 的 `timeoutMs`: `900_000` → `1_800_000`,新增 `reviewTimeoutMs: 900_000`
60
-
61
- 3. **REFACTOR_WORKFLOW_STEPS** (第 430-456 行)
62
- - `worker-reviewer` 的 `timeoutMs`: `900_000` → `1_800_000`,新增 `reviewTimeoutMs: 900_000`
63
- - `trimmer-reviewer` 的 `timeoutMs`: `300_000` → `1_200_000`,新增 `reviewTimeoutMs: 900_000`
64
-
65
- 4. **PERF_WORKFLOW_STEPS** (第 458-475 行)
66
- - `worker-reviewer` 的 `timeoutMs`: `900_000` → `1_800_000`,新增 `reviewTimeoutMs: 900_000`
67
-
68
- 5. **TEST_WORKFLOW_STEPS** (第 477-494 行)
69
- - `worker-reviewer` 的 `timeoutMs`: `900_000` → `1_800_000`,新增 `reviewTimeoutMs: 900_000`
70
-
71
- 6. **STYLE_WORKFLOW_STEPS** (第 513-523 行)
72
- - `trimmer-reviewer` 的 `timeoutMs`: `300_000` → `1_200_000`,新增 `reviewTimeoutMs: 900_000`
73
-
74
- - **验证方式**:检查所有 loop-group 配置都有对应的 `reviewTimeoutMs`,超时值符合需求
75
-
76
- ### 步骤 3:修改 `executeLoopGroup()` 中 reviewer 使用独立超时 + 每次循环后更新 UI
77
- - **前置条件**:步骤 1、2 完成
78
- - **改动文件**:`extensions/workflow-engine.ts`
79
- - **改动内容**(在 `executeLoopGroup` 函数内,约第 1197-1270 行):
80
-
81
- 1. **取出 reviewTimeoutMs**:在 while 循环之前,定义 `const reviewTimeoutMs = step.reviewTimeoutMs ?? step.timeoutMs;`
82
-
83
- 2. **修改 reviewer 的超时传递**:将 `runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, step.timeoutMs)` 改为 `runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, reviewTimeoutMs)`
84
-
85
- 3. **loopCount++ 后立即更新 UI**:在 `loopCount++;`(第 1243 行)之后,添加:
86
- ```typescript
87
- // 立即更新 UI 显示当前循环次数
88
- state.loopCount = loopCount;
89
- updateWidgetStep(stepIndex, step.label, "running", {
90
- loopCount,
91
- maxLoops: step.maxLoops,
92
- timeoutMs: step.timeoutMs,
93
- startedAt: step.startTime || Date.now(),
94
- });
95
- ```
96
- 注意:`step.startTime` 需要从外部传入或在函数内维护。`executeLoopGroup` 当前没有 `startTime` 参数。需要在 `executeWorkflowBackground` 中(第 1405 行)调用 `updateWidgetStep` 时已传入了 `startedAt`。方案是在 `executeLoopGroup` 中也接收 `startedAt` 或让 loop-group 步骤的 `startedAt` 能通过 `_widgetSteps[stepIndex]` 访问。
97
-
98
- 更优的方案:在 `executeLoopGroup` 中直接通过 `_widgetSteps[stepIndex]` 读取 `startedAt`。但模块变量 `_widgetSteps` 是 `workflow-engine.ts` 的私有变量,直接在 `executeLoopGroup` 中引用是合理的(已在同一模块中)。
99
-
100
- 4. **还原子步骤的 pending 状态**:在每次循环重新开始前,将 worker/reviewer 的 sub-step 状态重置为 "pending",以便 UI 显示新的迭代。
101
- ```typescript
102
- // 每次循环开始时重置 sub-step 状态
103
- setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
104
- setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
105
- ```
106
-
107
- - **验证方式**:运行测试,检查 widget 状态在每次循环后是否正确更新
108
-
109
- ### 步骤 4:修改 `ui-helpers.ts` 中 loop-group 行的超时显示
110
- - **前置条件**:无
111
- - **改动文件**:`extensions/ui-helpers.ts`
112
- - **改动内容**(在 `buildWidgetLines` 函数中,约第 455-456 行):
113
-
114
- 1. **loop-group 行不显示超时时间**:修改超时显示逻辑,当步骤是 loop-group 类型时跳过 timeout 显示。但由于 `buildWidgetLines` 不直接知道步骤类型,且 `WorkflowStepWidgetState` 中没有 `type` 字段,有两种方案:
115
- - **方案 A(推荐)**:在 `updateWidgetStep` 调用 loop-group 的 running/done 状态时,不传入 `timeoutMs`(设为 `undefined`),这样 `buildWidgetLines` 中 `s.timeoutMs` 为 `undefined`,`timeout` 字符串为空。
116
- - **方案 B**:在 `WorkflowStepWidgetState` 中增加 `type` 字段,但需要修改多个地方。
117
-
118
- 采用方案 A:`executeLoopGroup` 的 `updateWidgetStep` 调用中不传 `timeoutMs`,而在 worker/reviewer 的 sub-step 级别显示各自的超时。
119
-
120
- 2. **sub-step 级别显示超时**:在 sub-step 的 label 行(如 "worker ·" / "reviewer ·")后面附加超时时间。sub-step 的 `detail` 字段可用于此目的。在 `runAgentWithProgress` 中设置 sub-step 的 `detail` 或通过 `setWidgetSubStepStatus` 扩展接口。
121
-
122
- 具体做法:在 `runAgentWithProgress` 初始化 sub-step 时,将超时信息写入 `detail` 字段,例如 `worker · 超时时间30m`。
123
-
124
- - **验证方式**:视觉检查 widget 输出,确认 loop-group 行不再显示超时,sub-step 行显示正确
125
-
126
- ### 步骤 5:修改 `executeWorkflowBackground` 中 loop-group 步骤的 `updateWidgetStep` 调用
127
- - **前置条件**:步骤 4 完成
128
- - **改动文件**:`extensions/workflow-engine.ts`
129
- - **改动内容**:
130
-
131
- 在第 1405 行,当步骤类型为 `loop-group` 时,不传入 `timeoutMs`:
132
- ```typescript
133
- updateWidgetStep(currentStepIndex, step.label, "running", {
134
- maxLoops: step.maxLoops,
135
- startedAt: stepStartTime,
136
- // loop-group 行不显示 timeoutMs,由子代理 sub-step 显示
137
- });
138
- ```
139
-
140
- 在 done 状态更新时(第 1418 行),也不传 `timeoutMs`。
141
-
142
- - **验证方式**:在 widget 中确认 loop-group 行没有超时显示
143
-
144
- ### 步骤 6:更新 `runAgentWithProgress` 在 sub-step 中显示超时
145
- - **前置条件**:步骤 4 完成
146
- - **改动文件**:`extensions/workflow-engine.ts`
147
- - **改动内容**:
148
-
149
- 修改 `runAgentWithProgress` 函数中初始化 sub-step 的代码(约第 739-758 行),在 sub-step 的 `detail` 字段中写入超时时间。
150
-
151
- 由于 `runAgentWithProgress` 没有直接接收超时参数用于 UI 显示,可以在 sub-step 初始化时添加:
152
- ```typescript
153
- if (!existing) {
154
- step.subSteps.push({
155
- agent: agentName,
156
- status: "running",
157
- tools: [],
158
- outputs: [],
159
- startedAt: agentStartTime,
160
- detail: `超时时间${formatTimeout(timeoutMs)}`,
161
- });
162
- refreshWidget();
163
- }
164
- ```
165
-
166
- 注意需要 import `formatTimeout` 到 workflow-engine.ts,或将其从 ui-helpers.ts 导出。
167
-
168
- - **验证方式**:widget 中 sub-step 行应显示各自的超时时间
169
-
170
- ### 步骤 7:导出 `formatTimeout` 供 workflow-engine.ts 使用
171
- - **前置条件**:步骤 6 中的设计决策
172
- - **改动文件**:`extensions/ui-helpers.ts`
173
- - **改动内容**:
174
-
175
- 将 `formatTimeout` 函数前面加上 `export`(约第 427 行),使其可被其他模块导入使用。
176
-
177
- - **验证方式**:TypeScript 编译通过
178
-
179
- ### 步骤 8:编写测试文件 `test-loopcount-timeout-fix.mjs`
180
- - **前置条件**:所有代码修改完成
181
- - **改动文件**:`tests/test-loopcount-timeout-fix.mjs`(新增)
182
- - **改动内容**:编写测试覆盖以下场景:
183
- 1. **loopCount 更新测试**:模拟 `executeLoopGroup` 的 while 循环,验证每次 `loopCount++` 后 `updateWidgetStep` 被调用,且传入正确的 `loopCount` 值
184
- 2. **reviewer 独立超时测试**:验证 reviewer 调用 `runAgentWithProgress` 时使用 `reviewTimeoutMs` 而非 `step.timeoutMs`
185
- 3. **默认超时值测试**:静态分析 `dev-prompts.ts` 中所有 loop-group 配置,验证超时值是否符合需求(worker=30min, trimmer=20min, reviewer=15min)
186
- 4. **loop-group 行不显示 timeout 测试**:验证 `updateWidgetStep` 对 loop-group 步骤不传 `timeoutMs`
187
- 5. **sub-step 显示 timeout 测试**:验证 `runAgentWithProgress` 在新 sub-step 的 `detail` 中写入超时信息
188
- 6. **回归测试**:确保原有功能(非 loop-group 步骤的超时显示、完成状态等)不受影响
189
- - **验证方式**:运行 `node tests/test-loopcount-timeout-fix.mjs`
190
-
191
- ## 依赖关系
192
-
193
- - 步骤 1 是步骤 2、3 的前置条件(接口先定义才能使用)
194
- - 步骤 2 是步骤 3 的前置条件(配置中的 `reviewTimeoutMs` 值在步骤 3 中使用)
195
- - 步骤 4、5、6、7 可并行进行设计,但应顺序实施(步骤 4 → 步骤 7 → 步骤 6 → 步骤 5)
196
- - 步骤 8 在所有代码修改完成后进行
197
-
198
- ## 测试策略
199
-
200
- - **单元测试**:通过静态分析源代码模拟函数行为,验证 loopCount 更新逻辑、超时分离逻辑
201
- - **集成测试**:模拟完整的 `executeLoopGroup` 执行流程,检查 widget state 的每个字段
202
- - **回归测试**:运行已有的 `test-workflow-engine.mjs` 和 `test-workflow-engine-bugs.mjs`,确保无破坏
203
- - **手动测试**:在 TUI 中运行 `/dev-fix` 命令,观察 widget 中 loop-count 和超时显示是否正确
204
-
205
- ## 注意事项
206
-
207
- 1. **loopCount 初始值**:`executeLoopGroup` 中 `loopCount` 从 `loopCounts[step.id] ?? 0` 开始(第 1199 行)。这意味着第一次循环时 `loopCount` 为 0,在 `loopCount++` 后变为 1。UI 中第一次显示应该是"第 1 次循环",与现有 `buildWidgetLines` 中的硬编码一致。
208
-
209
- 2. **`_widgetSteps` 的访问**:`executeLoopGroup` 中需要读取 `_widgetSteps[stepIndex]` 来获取 `startedAt`。由于 `_widgetSteps` 是模块级变量,直接在函数中引用即可。
210
-
211
- 3. **sub-step 的重置**:每次循环开始时,需要将 worker 和 reviewer 的 sub-step 状态重置为 "pending",否则 UI 会显示上次循环的已完成状态。这通过 `setWidgetSubStepStatus` 实现。
212
-
213
- 4. **超时 UI 显示**:根据设计评审第 8 问的决策,loop-group 行不显示超时时间,超时只显示在 sub-step 级别。worker sub-step 显示"超时时间30m",reviewer sub-step 显示"超时时间15m"。
214
-
215
- 5. **`WORKFLOW_SUB_STEP_WIDGET_STATE.detail`**:利用现有的 `detail` 字段来显示 sub-step 的超时信息,无需添加新字段。
@@ -1,240 +0,0 @@
1
- # Grill/Input 文本换行与左方向键返回修复 — 实施计划
2
-
3
- ## 概述
4
-
5
- 修复 extensions/ 目录中 grill 和 dev-* 命令的三个 UI 问题:
6
- 1. dev-* 命令输入栏和 grill 自定义输入框不会自动换行(单行水平滚动),超长文本被截断
7
- 2. grill 提问回答环节的选项(a/b/c...)文字过长时不换行,内容被截断
8
- 3. grill 提问回答环节的 ← 左方向键返回上一题功能未实现
9
-
10
- ## 根因分析
11
-
12
- ### 问题 1:输入不换行
13
- `ui-helpers.ts` 中的 `uiInput` 函数使用 pi-tui 的 `Input` 组件。该组件是**单行文本输入**,支持水平滚动但不支持换行:`render()` 只输出一行文本,`handlePaste()` 会移除所有换行符。无法通过配置启用多行模式。
14
-
15
- ### 问题 2:选项不换行
16
- `grill-me-agent.ts` 中的 `showQuestionTUI` 使用 pi-tui 的 `SelectList` 组件显示选项(a/b/c...)。`SelectList.renderItem()` 使用 `truncateToWidth()` 将过长的 label **截断**而非**换行**。SelectList 不支持多行 item 渲染。
17
-
18
- ### 问题 3:左方向键不生效
19
- `showQuestionTUI` 的 `handleInput` 将键盘事件直接转发给 `selectList.handleInput(data)`。
20
- SelectList 只处理 `tui.select.up/down/confirm/cancel`(分别绑定了 ↑↓ Enter Esc),**不处理 `left` 键**(left 绑定的是 `tui.editor.cursorLeft`)。因此左方向键事件被静默丢弃。
21
-
22
- ## 文件清单
23
-
24
- ### 修改文件
25
- | 文件路径 | 改动描述 | 风险等级 |
26
- |---------|---------|---------|
27
- | `extensions/grill-me-agent.ts` | 1. 在 `showQuestionTUI.handleInput` 中拦截左方向键实现返回 2. 对过长选项使用 `description` 字段显示完整文本 3. 新增 import | 低 |
28
- | `extensions/ui-helpers.ts` | 1. `uiInput` 中添加实时换行预览区域 2. `uiInput.handleInput` 中拦截左方向键返回 | 低 |
29
-
30
- **不修改**:`@earendil-works/pi-tui` 库(SelectList、Input 组件保持不变)。
31
-
32
- ## 实施步骤
33
-
34
- ### 步骤 1:修复 `grill-me-agent.ts` — 左方向键返回 + 选项过长处理
35
-
36
- - **改动文件**:`extensions/grill-me-agent.ts`
37
- - **改动内容**:
38
-
39
- **a) 新增 import(文件头部,约第 13~15 行)**
40
- 添加:
41
- ```typescript
42
- import { matchesKey, Key, truncateToWidth } from "@earendil-works/pi-tui";
43
- ```
44
-
45
- **b) `showQuestionTUI` 中拦截左方向键(约第 670 行的 `handleInput`)**
46
- 当前代码仅做 `selectList.handleInput(data)`。左方向键传入 SelectList 后无效果。
47
-
48
- 修改为:先检查左方向键,若匹配则直接 `done("__BACK__")`,**不转发给 SelectList**:
49
- ```typescript
50
- return {
51
- render: (w) => container.render(w),
52
- invalidate: () => container.invalidate(),
53
- handleInput: (data) => {
54
- // 左方向键 → 返回上一题(SelectList 不处理 left 键,需自行拦截)
55
- if (backable && currentIndex > 1 && matchesKey(data, Key.left)) {
56
- done("__BACK__");
57
- return;
58
- }
59
- selectList.handleInput(data);
60
- tui.requestRender();
61
- },
62
- };
63
- ```
64
-
65
- **c) 选项文字过长处理(约第 640 行构建 `selectItems` 处)**
66
- SelectList 的 `renderItem` 对主列使用 `truncateToWidth` 截断。当选项文本超过主列宽度时,内容不可见。
67
-
68
- 修改方案:将完整 label **截断到合理长度**,将完整文本放入 `description` 字段。SelectList 在有 description 时会使用两列布局(主列 + description 列),让用户尽可能看到更多内容:
69
- ```typescript
70
- const MAX_OPTION_LABEL = 50;
71
- const selectItems: SelectItem[] = q.options.map((opt, i) => {
72
- const prefix = `(${String.fromCharCode(97 + i)}) `;
73
- const label = opt === previousAnswer
74
- ? `${prefix}${opt} - 上次选择`
75
- : `${prefix}${opt}`;
76
- const truncated = truncateToWidth(label, MAX_OPTION_LABEL, "...");
77
- return {
78
- value: `opt-${i}`,
79
- label: truncated,
80
- // 只有被截断时才提供 description,展示完整文本
81
- description: truncated !== label ? opt : undefined,
82
- };
83
- });
84
- ```
85
-
86
- **注意**:`truncateToWidth` 已经是 `@earendil-works/pi-tui` 的导出,可直接 import。
87
-
88
- - **验证方式**:
89
- - 运行 `/dev-fix` → 进入 grill → 在第一个问题上按 ← 键(此时 backable=false,currentIndex=1),不应触发返回
90
- - 进入第二个问题,按 ← 键,确认返回第一个问题
91
- - 确认选择了第一个问题的答案后,返回第一个问题,应看到" - 上次选择"标记
92
- - 对包含长选项文字的问题,确认 label 被截断但 description 列显示完整文本
93
-
94
- ### 步骤 2:修复 `ui-helpers.ts` — 输入框实时换行预览
95
-
96
- - **改动文件**:`extensions/ui-helpers.ts`
97
- - **改动内容**:
98
-
99
- 在 `uiInput` 函数的返回对象中(约第 230~245 行),当前的 `handleInput` 仅转发给 `input.handleInput(data)`。修改为:在转发后读取 `input.getValue()`,用 `wrapTextWithAnsi` 换行后更新一个预览 Text 组件。
100
-
101
- 完整修改如下(仅修改 `uiInput` 函数的返回部分):
102
-
103
- ```typescript
104
- // 在 Input 上方添加预览 Text(约第 215 行,在 input 创建前)
105
- const previewText = new Text("", 0, 0);
106
- container.addChild(previewText);
107
- container.addChild(new Spacer(1));
108
- // ... 创建 input ...
109
-
110
- // 修改 return 对象中的 handleInput(约第 230 行)
111
- return {
112
- render: (w) => container.render(w),
113
- invalidate: () => container.invalidate(),
114
- handleInput: (data) => {
115
- // 先让 Input 处理输入(更新内部 value)
116
- input.handleInput(data);
117
-
118
- // 读取更新后的 value,更新预览
119
- const val = input.getValue();
120
- if (val.length > 0) {
121
- const wrapped = wrapTextWithAnsi(val, width - 4);
122
- const previewContent = wrapped
123
- .map(l => theme.fg("dim", ` ${l}`))
124
- .join("\n");
125
- previewText.setText(previewContent);
126
- } else {
127
- previewText.setText("");
128
- }
129
-
130
- tui.requestRender();
131
- },
132
- };
133
- ```
134
-
135
- **注意**:
136
- - `previewText` 的初始状态应为空文本(`""`)
137
- - 每次按键后,`input.handleInput(data)` 会修改 Input 的内部 value,然后我们立即读取并更新预览
138
- - 使用 `theme.fg("dim", ...)` 使预览文本颜色较淡,与输入框区分
139
- - `wrapTextWithAnsi` 已经 import
140
-
141
- - **验证方式**:
142
- - 运行 `/dev-feat` → 在"核心功能描述"输入框输入超过终端宽度的长文本 → 确认上方出现换行预览
143
- - 按 Backspace 删除字符 → 确认预览实时更新
144
- - 按 Esc 取消 → 确认预览消失、输入被取消
145
- - 按 Enter 提交 → 确认正常提交
146
-
147
- ### 步骤 3:修复 `ui-helpers.ts` — 左方向键返回(自定义输入环节)
148
-
149
- - **改动文件**:`extensions/ui-helpers.ts`
150
- - **改动内容**:
151
-
152
- 在 `uiInput` 的 `handleInput` 中,**在调用 `input.handleInput(data)` 之前**拦截左方向键。当 `backable=true` 且按下左方向键时,返回 `BACK_MARKER`。
153
-
154
- 现有代码中已经支持 `Ctrl+Shift+←` 作为返回键(约第 240~244 行)。现在增加裸的 ← 键支持:
155
-
156
- ```typescript
157
- handleInput: (data) => {
158
- // [新增] 左方向键 → 返回(优先于 Input 的光标左移)
159
- if (backable && matchesKey(data, Key.left)) {
160
- done(BACK_MARKER);
161
- return;
162
- }
163
-
164
- // [原有] Ctrl+Shift+← → 返回
165
- if (backable && matchesKey(data, Key.ctrlShift("left"))) {
166
- done(BACK_MARKER);
167
- return;
168
- }
169
- // [原有] Ctrl+Shift+→ → 提交并继续
170
- if (backable && matchesKey(data, Key.ctrlShift("right"))) {
171
- done(input.getValue() || "");
172
- return;
173
- }
174
-
175
- // [原有] 转发给 Input 处理
176
- input.handleInput(data);
177
-
178
- // [新增] 更新预览(步骤 2)
179
- const val = input.getValue();
180
- if (val.length > 0) {
181
- const wrapped = wrapTextWithAnsi(val, width - 4);
182
- previewText.setText(
183
- wrapped.map(l => theme.fg("dim", ` ${l}`)).join("\n")
184
- );
185
- } else {
186
- previewText.setText("");
187
- }
188
-
189
- tui.requestRender();
190
- },
191
- ```
192
-
193
- **关于光标左移的替代方案**:拦截左方向键后,Input 组件中失去光标左移能力。但 pi-tui 的 keybinding 中 `tui.editor.cursorLeft` 同时绑定了 `left` 和 `ctrl+b`(见 keybindings.d.ts),用户可以使用 `Ctrl+B` 替代左方向键进行光标左移。这是合理的权衡。
194
-
195
- - **验证方式**:
196
- - 进入 grill → 选择"自定义输入" → 按 ← 键 → 确认返回到选项列表
197
- - 在非 grill 场景的普通输入(如 `/dev-feat` 的第一题)中按 ← 键 → 确认整个 wizard 被取消(因为 backable=true 但当前是第一题,返回即取消)
198
- - 确认 Ctrl+B 在输入框中仍可左移光标
199
-
200
- ### 步骤 4:更新 hints 文案(可选)
201
-
202
- - **改动文件**:`extensions/grill-me-agent.ts`(约第 663~667 行)
203
- - **改动内容**:
204
-
205
- 更新 hint 文本,说明左方向键也可用于返回:
206
-
207
- ```typescript
208
- const hint = backable && currentIndex > 1
209
- ? " ↑↓ 导航 • Enter 选择 • ← 返回上一题 • Esc 取消全部评审"
210
- : " ↑↓ 导航 • Enter 选择 • Esc 取消全部评审";
211
- ```
212
-
213
- - **验证方式**:检查 grill 问题界面底部提示文字是否正确显示
214
-
215
- ## 依赖关系
216
-
217
- - 步骤 1 与步骤 2、3 无依赖,可并行
218
- - 步骤 3 依赖步骤 2 的代码修改位置(handleInput 中需同时处理左方向键和预览更新)
219
- - 步骤 4 可选,在步骤 1 之后
220
-
221
- ## 测试策略
222
-
223
- 手动测试清单:
224
-
225
- | 测试场景 | 操作 | 预期结果 |
226
- |---------|------|---------|
227
- | Grill 左方向键返回 | 在 grill 第二个问题按 ← | 返回第一个问题,保留之前的选择 |
228
- | Grill 选项显示 | 包含长选项文字的问题 | label 截断有 ...,description 列显示完整文本 |
229
- | Grill 自定义输入返回 | 选自定义输入后按 ← | 返回到选项列表 |
230
- | dev-* 长文本预览 | 输入超长文本 | Input 上方显示换行后的完整文本预览 |
231
- | 普通 Esc 取消 | 按 Esc | 取消当前操作(不破坏原有功能) |
232
- | 上一步/下一步导航 | 使用 Ctrl+Shift+←/→ | 原有功能不受影响 |
233
- | 非 backable 左方向键 | 普通输入中按 ← | 光标在输入框中左移 |
234
-
235
- ## 注意事项
236
-
237
- - **不修改 pi-tui 库代码**,所有改动在 `extensions/` 目录内完成
238
- - **左方向键拦截**:在 SelectList 环境中,left 键对 SelectList 无意义(SelectList 只用 ↑↓ 导航),拦截 100% 安全。在 Input 环境中,左方向键的"光标左移"功能可用 Ctrl+B 替代
239
- - **预览性能**:每次按键触发 `wrapTextWithAnsi`,对数百字符的输入性能可忽略
240
- - **description 列**:SelectList 的 description 列本身也被 `truncateToWidth` 截断,但两列布局比单列能显示更多内容