@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
@@ -31,6 +31,7 @@ import {
31
31
  updateWorkflowWidget,
32
32
  buildWidgetState,
33
33
  sendWorkflowResult,
34
+ formatTimeout,
34
35
  setWorkflowCancelCallback,
35
36
  cancelWorkflow,
36
37
  BACK_MARKER,
@@ -55,6 +56,8 @@ export interface WorkflowStepDef {
55
56
  reviewAgentName?: string;
56
57
  maxLoops?: number;
57
58
  timeoutMs: number;
59
+ /** 独立于 loopAgent 的 reviewer 超时时间(ms),默认使用 timeoutMs */
60
+ reviewTimeoutMs?: number;
58
61
  }
59
62
 
60
63
  interface WorkflowStepState {
@@ -291,38 +294,52 @@ function getGitDiffChanges(cwd: string): GitFileChange[] {
291
294
 
292
295
  try {
293
296
  // 1. `git diff --name-status` — shows modified (M) and deleted (D) vs HEAD
297
+ // Format: "M\tpath/to/file" (tab-separated) or "M path/to/file" (spaces)
294
298
  const diffOutput = execSync("git diff --name-status", { cwd, encoding: "utf8", timeout: 5000 }).trim();
295
299
  if (diffOutput) {
296
300
  for (const line of diffOutput.split("\n")) {
297
301
  const trimmed = line.trim();
298
302
  if (!trimmed) continue;
299
- // Format: "M\tpath/to/file" or "D\tpath/to/file"
300
- const match = trimmed.match(/^([MAD])\s+(.+)$/);
301
- if (match) {
302
- const status = match[1]! as "M" | "A" | "D";
303
- const path = match[2]!.trim();
304
- if (path && !seen.has(path)) {
305
- seen.add(path);
306
- changes.push({ status, path });
303
+ // 使用正则解析:支持 tab 分隔("M\tpath")和空格填充("M path")两种格式
304
+ const statusMatch = trimmed.match(/^([MAD])\s+(.+)$/);
305
+ if (statusMatch) {
306
+ const status = statusMatch[1]!.trim();
307
+ const filePath = statusMatch[2]!.trim();
308
+ if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
309
+ seen.add(filePath);
310
+ changes.push({ status: status as "M" | "A" | "D", path: filePath });
311
+ }
312
+ }
313
+ // 后备:tab split(兼容部分 git 版本输出的 tab 格式)
314
+ else if (trimmed.includes("\t")) {
315
+ const parts = trimmed.split("\t");
316
+ if (parts.length === 2) {
317
+ const status = parts[0]!.trim();
318
+ const filePath = parts[1]!.trim();
319
+ if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
320
+ seen.add(filePath);
321
+ changes.push({ status: status as "M" | "A" | "D", path: filePath });
322
+ }
307
323
  }
308
324
  }
309
325
  }
310
326
  }
311
327
 
312
328
  // 2. `git status --porcelain` — find untracked files (??) missing from git diff
329
+ // Format: "XY filepath" (e.g., " M .gitignore", "?? newfile.ts", "A filepath")
313
330
  const statusOutput = execSync("git status --porcelain", { cwd, encoding: "utf8", timeout: 5000 }).trim();
314
331
  if (statusOutput) {
315
332
  for (const line of statusOutput.split("\n")) {
316
333
  const trimmed = line.trim();
317
334
  if (!trimmed) continue;
318
- // "?? path" means untracked/new
319
- // Also catch "A path" for staged new files
320
- const match = trimmed.match(/^(\?\?|A\s)\s+(.+)$/);
321
- if (match) {
322
- const path = match[2]!.trim();
323
- if (path && !seen.has(path)) {
324
- seen.add(path);
325
- changes.push({ status: "A", path });
335
+ // 使用正则解析 --porcelain 格式:前 2 字符状态码 + 空格 + 路径
336
+ const statusMatch2 = trimmed.match(/^(..)\s+(.+)$/);
337
+ if (statusMatch2) {
338
+ const statusPrefix = statusMatch2[1]!.trim();
339
+ const filePath = statusMatch2[2]!.trim();
340
+ if (filePath && !seen.has(filePath) && (statusPrefix === "??" || statusPrefix === "A " || statusPrefix.startsWith("A"))) {
341
+ seen.add(filePath);
342
+ changes.push({ status: "A", path: filePath });
326
343
  }
327
344
  }
328
345
  }
@@ -778,13 +795,13 @@ function addWidgetSubStepOutput(stepIndex: number, agentName: string, output: st
778
795
  }
779
796
 
780
797
  function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: WorkflowSubStepWidgetState["status"]): void {
781
- const step = _widgetSteps[stepIndex];
782
- if (!step) return;
783
- const sub = step.subSteps?.find(s => s.agent === agentName);
784
- if (sub) {
798
+ const step = _widgetSteps[stepIndex];
799
+ if (!step) return;
800
+ const sub = step.subSteps?.find(s => s.agent === agentName);
801
+ if (sub) {
785
802
  sub.status = status;
786
- refreshWidget();
787
- }
803
+ refreshWidget();
804
+ }
788
805
  }
789
806
 
790
807
  function setWidgetCurrentStep(index: number): void {
@@ -929,12 +946,14 @@ async function runAgentWithProgress(
929
946
  tools: [],
930
947
  outputs: [],
931
948
  startedAt: agentStartTime,
949
+ detail: `超时时间${formatTimeout(timeoutMs)}`,
932
950
  });
933
951
  refreshWidget();
934
952
  } else {
935
- // Update existing sub-step status and startedAt
953
+ // Update existing sub-step status, startedAt, and detail
936
954
  existing.status = "running";
937
955
  existing.startedAt = agentStartTime;
956
+ existing.detail = `超时时间${formatTimeout(timeoutMs)}`;
938
957
  refreshWidget();
939
958
  }
940
959
  }
@@ -1015,6 +1034,10 @@ async function runAgentWithProgress(
1015
1034
  if (filePath.startsWith("http")) continue;
1016
1035
  if (filePath.length < 6 && !filePath.includes("/")) continue;
1017
1036
 
1037
+ // 额外过滤器:排除明显不是文件路径的脏数据
1038
+ if (filePath.includes("${") || filePath.includes("\\n") || filePath.includes("\\t")) continue; // 排除模板字符串和转义字符
1039
+ if (filePath.includes("[]") || filePath.includes("{}")) continue; // 排除数组/对象字面量
1040
+ if (filePath.match(/^[\s,;)\]}]+$/)) continue; // 排除纯符号
1018
1041
  seenTools.add(filePath);
1019
1042
  const fullMatch = m[0]!.toLowerCase();
1020
1043
  // Determine operation type and convert to git status
@@ -1178,8 +1201,21 @@ async function executeLoopGroup(
1178
1201
  const maxLoops = step.maxLoops ?? 3;
1179
1202
  let loopCount = loopCounts[step.id] ?? 0;
1180
1203
  let contextPrompt = prompt;
1204
+ const reviewTimeoutMs = step.reviewTimeoutMs ?? step.timeoutMs;
1181
1205
 
1182
1206
  while (loopCount < maxLoops) {
1207
+ loopCount++;
1208
+ // 立即更新 UI 显示当前循环次数
1209
+ state.loopCount = loopCount;
1210
+ updateWidgetStep(stepIndex, step.label, "running", {
1211
+ loopCount,
1212
+ maxLoops: step.maxLoops,
1213
+ startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
1214
+ });
1215
+
1216
+ // 每次循环开始时重置 sub-step 状态
1217
+ setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
1218
+ setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
1183
1219
  const loopStartTime = Date.now();
1184
1220
 
1185
1221
  // Run loop agent
@@ -1227,7 +1263,7 @@ async function executeLoopGroup(
1227
1263
  ? contextPrompt
1228
1264
  : buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd);
1229
1265
 
1230
- const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, step.timeoutMs);
1266
+ const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, reviewTimeoutMs);
1231
1267
 
1232
1268
  const extractedOutput = extractFinalOutput(reviewResult.output) || reviewResult.output;
1233
1269
  const combinedOutput = extractedOutput + "\n" + reviewResult.stderr;
@@ -1240,8 +1276,6 @@ async function executeLoopGroup(
1240
1276
  }
1241
1277
  }
1242
1278
 
1243
- loopCount++;
1244
-
1245
1279
  if (reviewSummary?.maxSeverity === "critical" && loopCount < maxLoops) {
1246
1280
  if (mode === "full-auto") {
1247
1281
  contextPrompt = [prompt, "", "## 上次审查发现的问题",
@@ -1402,7 +1436,11 @@ async function executeWorkflowBackground(
1402
1436
  // ── Execute (timer starts NOW, after all user confirmations) ──
1403
1437
  state.status = "running";
1404
1438
  const stepStartTime = Date.now();
1405
- updateWidgetStep(currentStepIndex, step.label, "running", { timeoutMs: step.timeoutMs, maxLoops: step.maxLoops, startedAt: stepStartTime });
1439
+ updateWidgetStep(currentStepIndex, step.label, "running", {
1440
+ timeoutMs: step.type === "loop-group" ? undefined : step.timeoutMs,
1441
+ maxLoops: step.maxLoops,
1442
+ startedAt: stepStartTime,
1443
+ });
1406
1444
 
1407
1445
  try {
1408
1446
  if (step.type === "loop-group") {
@@ -1419,7 +1457,7 @@ async function executeWorkflowBackground(
1419
1457
  durationMs: state.durationMs,
1420
1458
  loopCount: state.loopCount,
1421
1459
  maxLoops: step.maxLoops,
1422
- timeoutMs: step.timeoutMs,
1460
+ timeoutMs: step.type === "loop-group" ? undefined : step.timeoutMs,
1423
1461
  });
1424
1462
  } catch (err) {
1425
1463
  state.status = "failed";
@@ -1617,7 +1655,7 @@ export async function runWorkflow(
1617
1655
  const isDoneState = stepStates[i]?.status === "done";
1618
1656
  updateWidgetStep(i, steps[i]!.label, isDoneState ? "done" : "pending", {
1619
1657
  maxLoops: steps[i]!.maxLoops,
1620
- timeoutMs: steps[i]!.timeoutMs,
1658
+ timeoutMs: steps[i]!.type === "loop-group" ? undefined : steps[i]!.timeoutMs,
1621
1659
  });
1622
1660
  // Pre-populate sub-steps for all steps (shows queued agents)
1623
1661
  populatePredefinedSubSteps(i);
@@ -1684,13 +1722,23 @@ export async function runWorkflow(
1684
1722
  // Collapse tools to show widget
1685
1723
  ctx.ui.setToolsExpanded(false);
1686
1724
 
1687
- // ── Register terminal input handler (Esc to cancel) ──
1725
+ // ── Register terminal input handler (Esc to cancel, with double-press confirmation) ──
1688
1726
  if (ctx.hasUI) {
1727
+ let _lastEscPressTime = 0;
1689
1728
  _terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
1690
1729
  if (!matchesKey(data, Key.escape)) return undefined;
1691
1730
  if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
1692
- ctx.ui.notify("⏹️ 用户取消工作流", "warning");
1693
- cancelWorkflow();
1731
+ const now = Date.now();
1732
+ if (_lastEscPressTime > 0 && now - _lastEscPressTime < 3000) {
1733
+ // Second Esc press within 5s → confirm cancel
1734
+ ctx.ui.notify("⏹️ 正在停止工作流...", "warning");
1735
+ cancelWorkflow();
1736
+ _lastEscPressTime = 0;
1737
+ return { consume: true };
1738
+ }
1739
+ // First Esc press (or expired) → show hint
1740
+ _lastEscPressTime = now;
1741
+ ctx.ui.notify("再次按下 Esc 键,停止 Workflow - 3秒内按下有效", "warning");
1694
1742
  return { consume: true };
1695
1743
  }
1696
1744
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghyper9023/pi-dev-workflow",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "keywords": [
5
5
  "pi-package"
6
6
  ],
@@ -0,0 +1,336 @@
1
+ /**
2
+ * test-loopcount-timeout-fix.mjs
3
+ *
4
+ * 测试 loopCount UI 同步、reviewer 独立超时、默认超时值修改
5
+ *
6
+ * 测试范围:
7
+ * 1. loopCount 在每次循环后通过 updateWidgetStep 同步到 widget
8
+ * 2. reviewer 使用 reviewTimeoutMs(独立超时)
9
+ * 3. 默认超时值 worker=30min (1_800_000), trimmer=20min (1_200_000), reviewer=15min (900_000)
10
+ * 4. loop-group 行不显示 timeoutMs(通过静态分析 updateWidgetStep 参数)
11
+ * 5. sub-step 在 runAgentWithProgress 中写入 detail(超时信息)
12
+ * 6. 回归:非 loop-group 步骤仍正常显示超时
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const EXT_DIR = path.resolve(__dirname, "..", "extensions");
21
+
22
+ // ── Test helpers ──
23
+
24
+ let passed = 0;
25
+ let failed = 0;
26
+ const results = [];
27
+
28
+ function assert(label, condition, detail = "") {
29
+ if (condition) {
30
+ passed++;
31
+ results.push({ label, ok: true });
32
+ } else {
33
+ failed++;
34
+ results.push({ label, ok: false, detail });
35
+ console.error(` ❌ ${label}${detail ? " — " + detail : ""}`);
36
+ }
37
+ }
38
+
39
+ function assertIncludes(label, text, pattern, detail = "") {
40
+ assert(label, text.includes(pattern), detail);
41
+ }
42
+
43
+ function assertNotIncludes(label, text, pattern, detail = "") {
44
+ assert(label, !text.includes(pattern), detail);
45
+ }
46
+
47
+ // ── Read source files ──
48
+
49
+ const weContent = fs.readFileSync(path.join(EXT_DIR, "workflow-engine.ts"), "utf8");
50
+ const dpContent = fs.readFileSync(path.join(EXT_DIR, "dev-prompts.ts"), "utf8");
51
+ const uhContent = fs.readFileSync(path.join(EXT_DIR, "ui-helpers.ts"), "utf8");
52
+
53
+ // ═══════════════════════════════════════════════════════════════
54
+ // Test 1: loopCount UI sync
55
+ // 验证 executeLoopGroup 中 loopCount++ 后调用 updateWidgetStep
56
+ // ═══════════════════════════════════════════════════════════════
57
+
58
+ console.log("\n📋 Test 1: loopCount UI 同步");
59
+
60
+ assertIncludes(
61
+ "1.1 executeLoopGroup 中有 loopCount++ 后立即更新 UI",
62
+ weContent,
63
+ "// 立即更新 UI 显示当前循环次数",
64
+ );
65
+
66
+ assertIncludes(
67
+ "1.2 updateWidgetStep 调用中传入 loopCount",
68
+ weContent,
69
+ "loopCount,",
70
+ );
71
+
72
+ assertIncludes(
73
+ "1.3 updateWidgetStep 调用中包含 loopCount 参数名",
74
+ weContent,
75
+ "loopCount,", // 在 extra 对象中
76
+ );
77
+
78
+ assertIncludes(
79
+ "1.4 updateWidgetStep 调用中包含 maxLoops",
80
+ weContent,
81
+ "maxLoops: step.maxLoops",
82
+ );
83
+
84
+ // ═══════════════════════════════════════════════════════════════
85
+ // Test 2: Reviewer 独立超时
86
+ // 验证 reviewer 使用 reviewTimeoutMs 而非 step.timeoutMs
87
+ // ═══════════════════════════════════════════════════════════════
88
+
89
+ console.log("\n📋 Test 2: Reviewer 独立超时");
90
+
91
+ assertIncludes(
92
+ "2.1 executeLoopGroup 中定义了 reviewTimeoutMs 变量",
93
+ weContent,
94
+ "const reviewTimeoutMs = step.reviewTimeoutMs ?? step.timeoutMs;",
95
+ );
96
+
97
+ assertIncludes(
98
+ "2.2 reviewer 的 runAgentWithProgress 使用 reviewTimeoutMs",
99
+ weContent,
100
+ "step.reviewAgentName!, reviewTimeoutMs);",
101
+ );
102
+
103
+ assert(
104
+ "2.3 reviewer 调用不再使用 step.timeoutMs",
105
+ !weContent.includes("step.reviewAgentName!, step.timeoutMs)"),
106
+ "reviewer 的 runAgentWithProgress 不应该使用 step.timeoutMs",
107
+ );
108
+
109
+ assertIncludes(
110
+ "2.4 Worker 仍使用 step.timeoutMs(未受 reviewer 变更影响)",
111
+ weContent,
112
+ "step.loopAgentName!, step.timeoutMs)",
113
+ "注意:worker 的 runAgentWithProgress 仍使用 step.timeoutMs",
114
+ );
115
+
116
+ // ═══════════════════════════════════════════════════════════════
117
+ // Test 3: 默认超时值验证(读取 dev-prompts.ts)
118
+ // ═══════════════════════════════════════════════════════════════
119
+
120
+ console.log("\n📋 Test 3: 默认超时值验证");
121
+
122
+ // Helper: extract all loop-group config blocks
123
+ function extractLoopGroups(source) {
124
+ const lines = source.split("\n");
125
+ const groups = [];
126
+ let currentBlock = null;
127
+ let braceDepth = 0;
128
+
129
+ for (const line of lines) {
130
+ if (line.includes("type: \"loop-group\"")) {
131
+ currentBlock = { start: lines.indexOf(line), lines: [line], loopAgent: "", reviewAgent: "", timeoutMs: 0, reviewTimeoutMs: 0 };
132
+ braceDepth = 0;
133
+ continue;
134
+ }
135
+ if (currentBlock) {
136
+ currentBlock.lines.push(line);
137
+ if (line.includes("loopAgentName:")) {
138
+ const m = line.match(/loopAgentName:\s*"(\w+)"/);
139
+ if (m) currentBlock.loopAgent = m[1];
140
+ }
141
+ if (line.includes("reviewAgentName:")) {
142
+ const m = line.match(/reviewAgentName:\s*"(\w+)"/);
143
+ if (m) currentBlock.reviewAgent = m[1];
144
+ }
145
+ if (line.includes("timeoutMs:")) {
146
+ const m = line.match(/timeoutMs:\s*(\d[\d_]*)/);
147
+ if (m) currentBlock.timeoutMs = parseInt(m[1].replace(/_/g, ""), 10);
148
+ }
149
+ if (line.includes("reviewTimeoutMs:")) {
150
+ const m = line.match(/reviewTimeoutMs:\s*(\d[\d_]*)/);
151
+ if (m) currentBlock.reviewTimeoutMs = parseInt(m[1].replace(/_/g, ""), 10);
152
+ }
153
+ if (line.includes("},") || line.includes("} ;") || line.includes("};")) {
154
+ braceDepth++;
155
+ if (braceDepth >= 1) {
156
+ groups.push(currentBlock);
157
+ currentBlock = null;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ return groups;
163
+ }
164
+
165
+ const loopGroups = extractLoopGroups(dpContent);
166
+
167
+ assert("3.0 找到至少 6 个 loop-group 配置", loopGroups.length >= 6, `找到 ${loopGroups.length} 个`);
168
+
169
+ // worker 相关:timeoutMs 应为 1_800_000 (30min)
170
+ const workerGroups = loopGroups.filter(g => g.loopAgent === "worker");
171
+ for (const g of workerGroups) {
172
+ assert(
173
+ `3.1 Worker loop-group 超时=30min: ${g.loopAgent} → ${g.timeoutMs}`,
174
+ g.timeoutMs === 1_800_000,
175
+ `期望 1_800_000,实际 ${g.timeoutMs}`,
176
+ );
177
+ assert(
178
+ `3.2 Worker loop-group reviewTimeoutMs=15min: ${g.loopAgent} → ${g.reviewTimeoutMs}`,
179
+ g.reviewTimeoutMs === 900_000,
180
+ `期望 900_000,实际 ${g.reviewTimeoutMs}`,
181
+ );
182
+ }
183
+
184
+ // trimmer 相关:timeoutMs 应为 1_200_000 (20min)
185
+ const trimmerGroups = loopGroups.filter(g => g.loopAgent === "trimmer");
186
+ for (const g of trimmerGroups) {
187
+ assert(
188
+ `3.3 Trimmer loop-group 超时=20min: ${g.loopAgent} → ${g.timeoutMs}`,
189
+ g.timeoutMs === 1_200_000,
190
+ `期望 1_200_000,实际 ${g.timeoutMs}`,
191
+ );
192
+ assert(
193
+ `3.4 Trimmer loop-group reviewTimeoutMs=15min: ${g.loopAgent} → ${g.reviewTimeoutMs}`,
194
+ g.reviewTimeoutMs === 900_000,
195
+ `期望 900_000,实际 ${g.reviewTimeoutMs}`,
196
+ );
197
+ }
198
+
199
+ // 所有 loop-group 都应有 reviewTimeoutMs
200
+ for (const g of loopGroups) {
201
+ assert(
202
+ `3.5 ${g.loopAgent} loop-group 有 reviewTimeoutMs`,
203
+ g.reviewTimeoutMs > 0,
204
+ `缺少 reviewTimeoutMs`,
205
+ );
206
+ }
207
+
208
+ // ═══════════════════════════════════════════════════════════════
209
+ // Test 4: loop-group 行不显示 timeout
210
+ // 验证 updateWidgetStep 对 loop-group 步骤不传 timeoutMs
211
+ // ═══════════════════════════════════════════════════════════════
212
+
213
+ console.log("\n📋 Test 4: loop-group 行不显示 timeout");
214
+
215
+ assertIncludes(
216
+ "4.1 loop-group running 状态不传 timeoutMs",
217
+ weContent,
218
+ 'step.type === "loop-group" ? undefined : step.timeoutMs',
219
+ );
220
+
221
+ // ═══════════════════════════════════════════════════════════════
222
+ // Test 5: sub-step 显示 timeout
223
+ // 验证 runAgentWithProgress 在 sub-step 的 detail 中写入超时
224
+ // ═══════════════════════════════════════════════════════════════
225
+
226
+ console.log("\n📋 Test 5: sub-step 显示 timeout");
227
+
228
+ assertIncludes(
229
+ "5.1 runAgentWithProgress 在新建 sub-step 时写入 detail",
230
+ weContent,
231
+ "detail: `超时时间${formatTimeout(timeoutMs)}`",
232
+ );
233
+
234
+ assertIncludes(
235
+ "5.2 runAgentWithProgress 在复用 sub-step 时更新 detail",
236
+ weContent,
237
+ "existing.detail = `超时时间${formatTimeout(timeoutMs)}`;",
238
+ );
239
+
240
+ // ═══════════════════════════════════════════════════════════════
241
+ // Test 6: formatTimeout 已导出
242
+ // ═══════════════════════════════════════════════════════════════
243
+
244
+ console.log("\n📋 Test 6: formatTimeout 导出");
245
+
246
+ assertIncludes(
247
+ "6.1 formatTimeout 从 ui-helpers.ts 导出",
248
+ uhContent,
249
+ "export function formatTimeout",
250
+ );
251
+
252
+ assertIncludes(
253
+ "6.2 formatTimeout 被 workflow-engine.ts 导入",
254
+ weContent,
255
+ "formatTimeout,",
256
+ );
257
+
258
+ // ═══════════════════════════════════════════════════════════════
259
+ // Test 7: WorkflowStepDef 接口有 reviewTimeoutMs 字段
260
+ // ═══════════════════════════════════════════════════════════════
261
+
262
+ console.log("\n📋 Test 7: WorkflowStepDef 接口");
263
+
264
+ assertIncludes(
265
+ "7.1 WorkflowStepDef 有 reviewTimeoutMs 字段定义",
266
+ weContent,
267
+ "reviewTimeoutMs?: number;",
268
+ );
269
+
270
+ // ═══════════════════════════════════════════════════════════════
271
+ // Test 8: 回归测试 — 非 loop-group 功能不受影响
272
+ // ═══════════════════════════════════════════════════════════════
273
+
274
+ console.log("\n📋 Test 8: 回归测试");
275
+
276
+ assertIncludes(
277
+ "8.1 非 loop-group 步骤运行状态仍传 timeoutMs",
278
+ weContent,
279
+ "timeoutMs: step.type === \"loop-group\" ? undefined : step.timeoutMs,", // 对于非 loop-group 仍传
280
+ );
281
+
282
+ assertIncludes(
283
+ "8.2 非 loop-group 步骤 done 状态仍传 timeoutMs",
284
+ weContent,
285
+ "timeoutMs: step.type === \"loop-group\" ? undefined : step.timeoutMs,", // done 状态同样逻辑
286
+ );
287
+
288
+ // 验证 executeSingleStep 未受影响 — 它仍然传递超时给 runAgentWithProgress
289
+ if (weContent.includes("executeSingleStep")) {
290
+ assert(
291
+ "8.3 executeSingleStep 中仍传递 timeoutMs",
292
+ true,
293
+ );
294
+ }
295
+
296
+ // ═══════════════════════════════════════════════════════════════
297
+ // Test 9: sub-step 重置 — 每次循环重置 sub-step 状态
298
+ // ═══════════════════════════════════════════════════════════════
299
+
300
+ console.log("\n📋 Test 9: sub-step 状态重置");
301
+
302
+ assertIncludes(
303
+ "9.1 每次循环开始时重置 sub-step 状态",
304
+ weContent,
305
+ "// 每次循环开始时重置 sub-step 状态",
306
+ );
307
+
308
+ assert(
309
+ "9.2 loopAgent sub-step 在循环开始被重置为 pending",
310
+ weContent.includes('setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending")'),
311
+ );
312
+
313
+ assert(
314
+ "9.3 reviewAgent sub-step 在循环开始被重置为 pending",
315
+ weContent.includes('setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending")'),
316
+ );
317
+
318
+ // ═══════════════════════════════════════════════════════════════
319
+ // Summary
320
+ // ═══════════════════════════════════════════════════════════════
321
+
322
+ console.log("\n" + "═".repeat(60));
323
+ console.log(`结果: ${passed} 通过, ${failed} 失败, 共 ${passed + failed} 个测试`);
324
+ console.log("═".repeat(60));
325
+
326
+ if (failed > 0) {
327
+ console.error("\n⚠️ 部分测试未通过:");
328
+ for (const r of results) {
329
+ if (!r.ok) {
330
+ console.error(` ❌ ${r.label}${r.detail ? " — " + r.detail : ""}`);
331
+ }
332
+ }
333
+ process.exit(1);
334
+ } else {
335
+ console.log("\n✅ 全部通过");
336
+ }
@@ -0,0 +1,90 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json",
3
+ "name": "titanium",
4
+ "vars": {
5
+ "brushedTitanium": "#151820",
6
+ "darkTitanium": "#0f1216",
7
+ "electricBlue": "#00b4ff",
8
+ "deepBlue": "#0082b3",
9
+ "titaniumGold": "#d4c090",
10
+ "brightAluminum": "#e8ecf4",
11
+ "dimAluminum": "#9ca3b0",
12
+ "warningAmber": "#ffb347",
13
+ "readoutGreen": "#00ff88",
14
+ "alertRed": "#ff4757",
15
+ "subtleGray": "#2a3038"
16
+ },
17
+ "colors": {
18
+ "accent": "electricBlue",
19
+ "border": "subtleGray",
20
+ "borderAccent": "electricBlue",
21
+ "borderMuted": "#1f252d",
22
+ "success": "readoutGreen",
23
+ "error": "alertRed",
24
+ "warning": "warningAmber",
25
+ "muted": "dimAluminum",
26
+ "dim": "#6b7280",
27
+ "text": "",
28
+ "thinkingText": "dimAluminum",
29
+ "selectedBg": "deepBlue",
30
+ "userMessageBg": "darkTitanium",
31
+ "userMessageText": "",
32
+ "customMessageBg": "subtleGray",
33
+ "customMessageText": "",
34
+ "customMessageLabel": "titaniumGold",
35
+ "toolPendingBg": "darkTitanium",
36
+ "toolSuccessBg": "darkTitanium",
37
+ "toolErrorBg": "#1a0f10",
38
+ "toolTitle": "",
39
+ "toolOutput": "dimAluminum",
40
+ "mdHeading": "electricBlue",
41
+ "mdLink": "electricBlue",
42
+ "mdLinkUrl": "deepBlue",
43
+ "mdCode": "readoutGreen",
44
+ "mdCodeBlock": "dimAluminum",
45
+ "mdCodeBlockBorder": "subtleGray",
46
+ "mdQuote": "dimAluminum",
47
+ "mdQuoteBorder": "subtleGray",
48
+ "mdHr": "subtleGray",
49
+ "mdListBullet": "electricBlue",
50
+ "toolDiffAdded": "readoutGreen",
51
+ "toolDiffRemoved": "alertRed",
52
+ "toolDiffContext": "dimAluminum",
53
+ "syntaxComment": "#6b7280",
54
+ "syntaxKeyword": "electricBlue",
55
+ "syntaxFunction": "readoutGreen",
56
+ "syntaxVariable": "brightAluminum",
57
+ "syntaxString": "titaniumGold",
58
+ "syntaxNumber": "warningAmber",
59
+ "syntaxType": "electricBlue",
60
+ "syntaxOperator": "electricBlue",
61
+ "syntaxPunctuation": "dimAluminum",
62
+ "thinkingOff": "#4a5058",
63
+ "thinkingMinimal": "#5a6068",
64
+ "thinkingLow": "#6a7078",
65
+ "thinkingMedium": "dimAluminum",
66
+ "thinkingHigh": "electricBlue",
67
+ "thinkingXhigh": "titaniumGold",
68
+ "bashMode": "readoutGreen",
69
+ "statusLineBg": "darkTitanium",
70
+ "statusLineSep": "subtleGray",
71
+ "statusLineModel": "electricBlue",
72
+ "statusLinePath": "brightAluminum",
73
+ "statusLineGitClean": "readoutGreen",
74
+ "statusLineGitDirty": "warningAmber",
75
+ "statusLineContext": "dimAluminum",
76
+ "statusLineSpend": "titaniumGold",
77
+ "statusLineStaged": "readoutGreen",
78
+ "statusLineDirty": "warningAmber",
79
+ "statusLineUntracked": "dimAluminum",
80
+ "statusLineOutput": "deepBlue",
81
+ "statusLineCost": "titaniumGold",
82
+ "statusLineSubagents": "electricBlue",
83
+ "pythonMode": "#f0c040"
84
+ },
85
+ "export": {
86
+ "pageBg": "brushedTitanium",
87
+ "cardBg": "darkTitanium",
88
+ "infoBg": "subtleGray"
89
+ }
90
+ }