@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
@@ -20,6 +20,41 @@
20
20
 
21
21
  import * as fs from "node:fs";
22
22
  import * as path from "node:path";
23
+ import { randomBytes } from "node:crypto";
24
+
25
+ /**
26
+ * Generate a UUID v7 (time-ordered) string.
27
+ * Format: tttttttt-tttt-7xxx-yxxx-xxxxxxxxxxxx
28
+ * - t = Unix timestamp (ms) as 48-bit big-endian
29
+ * - 7 = version (0111)
30
+ * - y = variant (10xx = 8, 9, a, b)
31
+ * - x = random
32
+ */
33
+ function uuidv7(): string {
34
+ const ts = BigInt(Date.now());
35
+ const buf = new Uint8Array(16);
36
+ // 48-bit timestamp (big-endian) in bytes 0-5
37
+ buf[0] = Number((ts >> 40n) & 0xFFn);
38
+ buf[1] = Number((ts >> 32n) & 0xFFn);
39
+ buf[2] = Number((ts >> 24n) & 0xFFn);
40
+ buf[3] = Number((ts >> 16n) & 0xFFn);
41
+ buf[4] = Number((ts >> 8n) & 0xFFn);
42
+ buf[5] = Number(ts & 0xFFn);
43
+ // 10 random bytes for version/variant/rand
44
+ const r = randomBytes(10);
45
+ // Byte 6: version (0111) | rand_a high 4 bits
46
+ buf[6] = 0x70 | (r[0] >> 4);
47
+ // Byte 7: rand_a low 8 bits
48
+ buf[7] = (r[0] << 4) | (r[1] >> 4);
49
+ // Byte 8: variant (10) | rand_b high 6 bits
50
+ buf[8] = 0x80 | (r[2] >> 2);
51
+ // Bytes 9-15: remaining rand_b (56 bits)
52
+ buf[9] = r[3]; buf[10] = r[4]; buf[11] = r[5];
53
+ buf[12] = r[6]; buf[13] = r[7]; buf[14] = r[8]; buf[15] = r[9];
54
+ // Format as 8-4-4-4-12 hex UUID
55
+ const hex = Array.from(buf, b => b.toString(16).padStart(2, '0')).join('');
56
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
57
+ }
23
58
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
24
59
  import { Key, matchesKey } from "@earendil-works/pi-tui";
25
60
  import { execSync } from "child_process";
@@ -95,6 +130,8 @@ interface BaselineEntry {
95
130
 
96
131
  interface CheckpointData {
97
132
  version: 2;
133
+ /** UUID that uniquely identifies this workflow run across all output files */
134
+ workflowId: string;
98
135
  createdAt: string;
99
136
  updatedAt: string;
100
137
  prompt: string;
@@ -372,7 +409,7 @@ function toGitStatus(toolType: string): string {
372
409
  */
373
410
  function hasContentChanged(cwd: string, path: string, baselineHash: string): boolean {
374
411
  try {
375
- const currentHash = require('child_process').spawnSync('git', ['hash-object', path], { cwd, encoding: 'utf8', timeout: 3000 }).stdout?.trim() || "";
412
+ const currentHash = execSync(`git hash-object "${path}"`, { cwd, encoding: 'utf8', timeout: 3000 }).trim();
376
413
  return currentHash !== baselineHash;
377
414
  } catch {
378
415
  // file deleted or inaccessible — consider changed
@@ -486,16 +523,17 @@ export function loadCheckpointFromFile(cwd: string): CheckpointData | null {
486
523
  }
487
524
 
488
525
  /**
489
- * Archive checkpoint after completion: rename to checkpoint-<plan-id>.json
526
+ * Archive checkpoint after completion: rename to checkpoint-<uuid>-<plan-id>.json
490
527
  */
491
528
  export function archiveCheckpointFile(cwd: string, planFileRelPath?: string): void {
492
529
  try {
493
530
  const cpPath = path.join(cwd, CHECKPOINT_FILE);
494
531
  if (!fs.existsSync(cpPath)) return;
532
+ const uuidPart = _workflowId ? `-${_workflowId}` : "";
495
533
  const planId = planFileRelPath
496
534
  ? path.basename(planFileRelPath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_")
497
535
  : `archive-${Date.now().toString(36)}`;
498
- const archiveName = `checkpoint-${planId}.json`;
536
+ const archiveName = `checkpoint${uuidPart}-${planId}.json`;
499
537
  const archiveDir = path.join(cwd, DEV_OUTPUT_DIR, "pi-workflow");
500
538
  fs.renameSync(cpPath, path.join(archiveDir, archiveName));
501
539
  } catch { /* ignore */ }
@@ -510,25 +548,43 @@ function buildTaskForStep(
510
548
  prompt: string,
511
549
  planFileRelPath: string | undefined,
512
550
  cwd: string,
551
+ workflowId: string,
552
+ chainContext?: string,
513
553
  ): string {
514
554
  if (agentName === "planner") {
515
555
  return [
516
556
  "请根据以下功能需求,分析代码库结构,生成详细的实施计划,并写入 .pi-dev-output/pi-plans/ 目录。",
517
557
  "",
558
+ "## 文件名格式",
559
+ `<YYYYMMDD-HHmmss>-<简短功能名>-${workflowId}.md`,
560
+ "请在文件名末尾添加工作流 UUID,以便同一工作流的所有文件可关联追溯。",
561
+ "",
518
562
  "## 功能需求",
519
563
  prompt,
564
+ ...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
520
565
  ].join("\n");
521
566
  }
522
567
  if (agentName === "worker") {
523
568
  const planContent = planFileRelPath ? readFileContent(cwd, planFileRelPath) : undefined;
569
+ const planLocation = planFileRelPath
570
+ ? `\n实施计划文件位于: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 目录中 grep UUID "${workflowId}" 找到)`
571
+ : "";
524
572
  if (planContent) {
525
573
  return [
526
574
  "请根据以下实施计划逐步实现代码改动。",
527
575
  "",
528
576
  "## 实施计划",
529
577
  planContent,
578
+ planLocation,
579
+ "",
580
+ "## 原始需求与修改反馈",
581
+ prompt,
582
+ ...(chainContext ? ["", chainContext] : []),
530
583
  "",
531
584
  "请严格按照计划中的步骤实施,不要做计划外的修改。",
585
+ "",
586
+ "实施完成后,在输出中列出你修改的所有文件,供后续审查者参考。",
587
+ ...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
532
588
  ].join("\n");
533
589
  }
534
590
  return [
@@ -536,17 +592,26 @@ function buildTaskForStep(
536
592
  "",
537
593
  "## 功能需求",
538
594
  prompt,
595
+ planLocation,
596
+ ...(chainContext ? ["", chainContext] : []),
539
597
  "",
540
598
  "请先分析代码库,制定简要计划,再逐步实施。",
599
+ "实施完成后,在输出中列出你修改的所有文件,供后续审查者参考。",
600
+ ...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
541
601
  ].join("\n");
542
602
  }
543
603
  if (agentName === "trimmer") {
604
+ const planContent = planFileRelPath ? readFileContent(cwd, planFileRelPath) : undefined;
544
605
  return [
545
606
  "请精简当前代码库的代码。",
546
607
  "缩短不必要的冗长行,优化可读性,消除可合并的重复逻辑。",
608
+ "注意:以下实施计划列出了本次工作流的核心新增内容,精简时请确保不影响这些改动。",
547
609
  "",
548
610
  "## 原始功能需求",
549
611
  prompt,
612
+ ...(planContent ? ["", "## 实施计划(改动范围)", planContent] : []),
613
+ ...(chainContext ? ["", chainContext] : []),
614
+ ...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
550
615
  ].join("\n");
551
616
  }
552
617
  if (agentName === "docWriter") {
@@ -559,6 +624,8 @@ function buildTaskForStep(
559
624
  "## 功能需求",
560
625
  prompt,
561
626
  planContent,
627
+ ...(chainContext ? ["", chainContext] : []),
628
+ ...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
562
629
  ].join("\n");
563
630
  }
564
631
  return prompt;
@@ -568,21 +635,35 @@ function buildReviewTask(
568
635
  prompt: string,
569
636
  planFileRelPath: string | undefined,
570
637
  cwd: string,
638
+ workflowId: string,
639
+ chainContext?: string,
571
640
  ): string {
572
641
  const planContent = planFileRelPath ? readFileContent(cwd, planFileRelPath) : undefined;
573
642
  const parts = [
574
643
  "请审查当前代码库中针对以下功能的实现。",
575
644
  "检查是否有 bug、逻辑错误、未完成的功能、代码质量问题。",
576
645
  "将详细审查报告写入 .pi-dev-output/pi-review/md/ 目录。",
646
+ "",
647
+ "## 文件名格式",
648
+ `review-<YYYYMMDD-HHmmss>-${workflowId}.md`,
649
+ "请在文件名末尾添加工作流 UUID,以便同一工作流的所有文件可关联追溯。",
650
+ "",
577
651
  "在回复末尾输出以下格式的结构化摘要(必须包含):",
578
652
  "[REVIEW_SUMMARY]",
579
653
  '{"maxSeverity":"critical|medium|low","critical":N,"medium":N,"low":N}',
580
654
  "[/REVIEW_SUMMARY]",
581
655
  "",
656
+ "⚠️ 重要:这个 JSON 摘要不是可选的,而是强制性的。",
657
+ "如果缺少此 JSON 摘要,工作流将无法判断是否需要继续修复循环。",
658
+ "请在回复的末尾单独输出,确保前后无其他文本(除换行符外)。",
659
+ "注意最大严重级别字段名是 maxSeverity(注意大小写)。",
660
+ "",
582
661
  "## 功能需求",
583
662
  prompt,
584
663
  ];
585
664
  if (planContent) parts.push("", "## 实施计划", planContent);
665
+ if (chainContext) parts.push("", chainContext);
666
+ if (buildWorkflowInfoBlock()) parts.push("", buildWorkflowInfoBlock());
586
667
  return parts.join("\n");
587
668
  }
588
669
 
@@ -613,6 +694,10 @@ let _workflowAgentRunHistory: AgentRunEntry[] = [];
613
694
  let _workflowStepDefs: WorkflowStepDef[] = [];
614
695
  /** Baseline snapshot: git hashes of dirty files at workflow start */
615
696
  let _workflowBaseline: BaselineEntry[] = [];
697
+ /** Workflow UUID for cross-file traceability */
698
+ let _workflowId = "";
699
+ /** Chain context: keyed parts of previous agent output to pass to next agent */
700
+ let _chainContextParts: Record<string, string> = {};
616
701
 
617
702
  let _widgetMode: WorkflowMode = "attended";
618
703
  let _widgetSteps: WorkflowStepWidgetState[] = [];
@@ -623,6 +708,58 @@ let _widgetExtraTokenCount = 0;
623
708
  let _workflowRunning = false;
624
709
  let _cleanupTimer: ReturnType<typeof setTimeout> | null = null;
625
710
 
711
+ // ── Chain context helpers ──────────────────────────────────────
712
+
713
+ /**
714
+ * Update a named part of the chain context. Each key is a section header;
715
+ * calling with the same key overwrites the previous value.
716
+ */
717
+ function updateChainContext(key: string, content: string): void {
718
+ if (content) {
719
+ _chainContextParts[key] = content;
720
+ } else {
721
+ delete _chainContextParts[key];
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Build the chain context string from all parts, for injection into
727
+ * buildTaskForStep / buildReviewTask prompts.
728
+ */
729
+ function buildChainContext(): string {
730
+ const parts = Object.entries(_chainContextParts)
731
+ .filter(([_, v]) => v)
732
+ .map(([k, v]) => `## ${k}\n${v}`);
733
+ return parts.length > 0 ? parts.join("\n\n") : "";
734
+ }
735
+
736
+ /**
737
+ * Reset chain context — called at the start of a new workflow.
738
+ */
739
+ function resetChainContext(): void {
740
+ _chainContextParts = {};
741
+ }
742
+
743
+ /**
744
+ * Build a standard Workflow Info block injected into every subagent prompt.
745
+ */
746
+ function buildWorkflowInfoBlock(): string {
747
+ if (!_workflowId) return "";
748
+ const modeLabel = _widgetMode === "full-auto" ? "全自动"
749
+ : _widgetMode === "full-attended" ? "完全值守"
750
+ : "值守";
751
+ return [
752
+ "## 工作流信息",
753
+ `- 工作流 UUID: ${_workflowId}`,
754
+ `- 工作流类型: ${_workflowType ?? "通用"}`,
755
+ `- 工作流启动时间: ${_workflowCreatedAt}`,
756
+ `- 工作流模式: ${modeLabel}`,
757
+ "",
758
+ "提示:如果需要在 .pi-dev-output/ 下查找属于本工作流的文件,",
759
+ `请在工作流输出目录中搜索工作流 UUID "${_workflowId}"。`,
760
+ ].join("\n");
761
+ }
762
+
626
763
  function refreshWidget(): void {
627
764
  if (!_lastWorkflowCtx) return;
628
765
  const taskSummary = extractTaskSummary(_workflowPrompt);
@@ -637,6 +774,7 @@ function refreshWidget(): void {
637
774
  "running",
638
775
  { toolCount: _widgetExtraToolCount, tokenCount: _widgetExtraTokenCount },
639
776
  taskSummary,
777
+ _workflowId,
640
778
  );
641
779
  updateWorkflowWidget(_lastWorkflowCtx, widgetState);
642
780
  }
@@ -694,12 +832,19 @@ function populatePredefinedSubSteps(stepIndex: number): void {
694
832
  if (step.subSteps && step.subSteps.length > 0) return; // already populated
695
833
 
696
834
  const def = _workflowStepDefs[stepIndex]!;
835
+ const agents = discoverAgents();
836
+
837
+ function agentThinkingLevel(name: string): string | undefined {
838
+ return agents.find(a => a.name === name)?.thinkingLevel;
839
+ }
840
+
697
841
  const newSubSteps: WorkflowSubStepWidgetState[] = [];
698
842
 
699
843
  if (def.type === "loop-group") {
700
844
  if (def.loopAgentName) {
701
845
  newSubSteps.push({
702
846
  agent: def.loopAgentName,
847
+ thinkingLevel: agentThinkingLevel(def.loopAgentName),
703
848
  status: "pending",
704
849
  tools: [],
705
850
  outputs: [],
@@ -708,6 +853,7 @@ function populatePredefinedSubSteps(stepIndex: number): void {
708
853
  if (def.reviewAgentName) {
709
854
  newSubSteps.push({
710
855
  agent: def.reviewAgentName,
856
+ thinkingLevel: agentThinkingLevel(def.reviewAgentName),
711
857
  status: "pending",
712
858
  tools: [],
713
859
  outputs: [],
@@ -716,6 +862,7 @@ function populatePredefinedSubSteps(stepIndex: number): void {
716
862
  } else if (def.agentName) {
717
863
  newSubSteps.push({
718
864
  agent: def.agentName,
865
+ thinkingLevel: agentThinkingLevel(def.agentName),
719
866
  status: "pending",
720
867
  tools: [],
721
868
  outputs: [],
@@ -804,6 +951,24 @@ function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: Wo
804
951
  }
805
952
  }
806
953
 
954
+ /**
955
+ * 重置子步骤为 pending 状态并清除计时信息(durationMs/startedAt)。
956
+ * 用于循环组中开启新循环时,清除上一轮的计时数据。
957
+ */
958
+ function resetWidgetSubStepTimers(stepIndex: number, agentName: string): void {
959
+ const step = _widgetSteps[stepIndex];
960
+ if (!step) return;
961
+ const sub = step.subSteps?.find(s => s.agent === agentName);
962
+ if (sub) {
963
+ sub.status = "pending";
964
+ sub.durationMs = undefined;
965
+ sub.startedAt = undefined;
966
+ sub.tools = [];
967
+ sub.outputs = [];
968
+ }
969
+ refreshWidget();
970
+ }
971
+
807
972
  function setWidgetCurrentStep(index: number): void {
808
973
  _widgetCurrentIdx = index;
809
974
  refreshWidget();
@@ -984,7 +1149,7 @@ async function runAgentWithProgress(
984
1149
  addWidgetSubStepOutput(stepIndex, agentName, pathCandidate);
985
1150
  }
986
1151
  }
987
- });
1152
+ }, _workflowId ? { workflowId: _workflowId } : undefined);
988
1153
 
989
1154
  const agentDuration = Date.now() - agentStartTime;
990
1155
 
@@ -1128,6 +1293,14 @@ async function runAgentWithProgress(
1128
1293
  "done";
1129
1294
  setWidgetSubStepStatus(stepIndex, agentName, subStatus);
1130
1295
 
1296
+ // ⭐ 修复:代理完成后设置最终持续时长并清除 startedAt
1297
+ // 确保 UI 使用记录的 durationMs 而非实时 Date.now() - startedAt
1298
+ const completedSub = _widgetSteps[stepIndex]?.subSteps?.find(s => s.agent === agentName);
1299
+ if (completedSub) {
1300
+ completedSub.durationMs = agentDuration;
1301
+ completedSub.startedAt = undefined;
1302
+ }
1303
+
1131
1304
  return result;
1132
1305
  }
1133
1306
 
@@ -1149,7 +1322,7 @@ async function executeSingleStep(
1149
1322
  const agent = agentMap.get(agentName);
1150
1323
  if (!agent) throw new Error(`未找到 agent: ${agentName}`);
1151
1324
 
1152
- const task = buildTaskForStep(agentName, prompt, planFileRelPath, _workflowCwd);
1325
+ const task = buildTaskForStep(agentName, prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
1153
1326
  let retried = false;
1154
1327
 
1155
1328
  let result = await runAgentWithProgress(agent, task, stepIndex, agentName, step.timeoutMs);
@@ -1176,6 +1349,35 @@ async function executeSingleStep(
1176
1349
  if (result.exitCode !== 0 && result.stderr) {
1177
1350
  throw new Error(`Agent 错误 (exit ${result.exitCode}): ${result.stderr.slice(0, 500)}`);
1178
1351
  }
1352
+
1353
+ // ── Capture chain context for single-step agents ──
1354
+ const agentChanges = _workflowFileChanges
1355
+ .filter(c => c.stepIndex === stepIndex && c.agent === agentName)
1356
+ .map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
1357
+
1358
+ let chainKey: string;
1359
+ if (agentName === "planner") chainKey = "计划制定摘要";
1360
+ else if (agentName === "docWriter") chainKey = "文档更新摘要";
1361
+ else chainKey = `${agentName} 执行摘要`;
1362
+
1363
+ if (agentChanges.length > 0) {
1364
+ const editCount = agentChanges.filter(c => c.startsWith("M")).length;
1365
+ const newCount = agentChanges.filter(c => c.startsWith("A")).length;
1366
+ const delCount = agentChanges.filter(c => c.startsWith("D")).length;
1367
+ const statsParts: string[] = [];
1368
+ if (editCount > 0) statsParts.push(`修改 ${editCount} 个`);
1369
+ if (newCount > 0) statsParts.push(`新增 ${newCount} 个`);
1370
+ if (delCount > 0) statsParts.push(`删除 ${delCount} 个`);
1371
+ const statsLine = statsParts.length > 0 ? `改动统计: ${statsParts.join(",")}\n\n` : "";
1372
+
1373
+ updateChainContext(chainKey,
1374
+ `${agentName} 已完成。\n` +
1375
+ statsLine +
1376
+ `变更文件列表:\n${agentChanges.join("\n")}`
1377
+ );
1378
+ } else {
1379
+ updateChainContext(chainKey, `${agentName} 已完成执行,未检测到文件变更。`);
1380
+ }
1179
1381
  }
1180
1382
 
1181
1383
  // ═══════════════════════════════════════════════════════════════
@@ -1213,13 +1415,13 @@ async function executeLoopGroup(
1213
1415
  startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
1214
1416
  });
1215
1417
 
1216
- // 每次循环开始时重置 sub-step 状态
1217
- setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
1218
- setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
1418
+ // 每次循环开始时重置 sub-step 状态(清除上一轮的计时和工具记录)
1419
+ resetWidgetSubStepTimers(stepIndex, step.loopAgentName!);
1420
+ resetWidgetSubStepTimers(stepIndex, step.reviewAgentName!);
1219
1421
  const loopStartTime = Date.now();
1220
1422
 
1221
- // Run loop agent
1222
- const loopTask = buildTaskForStep(step.loopAgentName!, contextPrompt, planFileRelPath, _workflowCwd);
1423
+ // ── Run loop agent (worker / trimmer) ──
1424
+ const loopTask = buildTaskForStep(step.loopAgentName!, contextPrompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
1223
1425
 
1224
1426
  let agentResult = await runAgentWithProgress(loopAgent, loopTask, stepIndex, step.loopAgentName!, step.timeoutMs);
1225
1427
 
@@ -1238,9 +1440,40 @@ async function executeLoopGroup(
1238
1440
  }
1239
1441
  }
1240
1442
 
1443
+ // ── After loop agent completes: capture changes + stats for chain context ──
1444
+ const loopAgentChanges = _workflowFileChanges
1445
+ .filter(c => c.stepIndex === stepIndex && c.agent === step.loopAgentName)
1446
+ .map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
1447
+
1448
+ // Use agent-specific keys so worker and trimmer changes coexist without overwriting
1449
+ const chainKey = step.loopAgentName === "worker" ? "代码实施摘要" : "代码精简摘要";
1450
+
1451
+ if (loopAgentChanges.length > 0) {
1452
+ const editCount = loopAgentChanges.filter(c => c.startsWith("M")).length;
1453
+ const newCount = loopAgentChanges.filter(c => c.startsWith("A")).length;
1454
+ const delCount = loopAgentChanges.filter(c => c.startsWith("D")).length;
1455
+ const statsParts: string[] = [];
1456
+ if (editCount > 0) statsParts.push(`修改 ${editCount} 个`);
1457
+ if (newCount > 0) statsParts.push(`新增 ${newCount} 个`);
1458
+ if (delCount > 0) statsParts.push(`删除 ${delCount} 个`);
1459
+ const statsLine = statsParts.length > 0 ? `改动统计: ${statsParts.join(",")}\n\n` : "";
1460
+
1461
+ updateChainContext(chainKey,
1462
+ `${step.loopAgentName === "worker" ? "代码实施" : "代码精简"}已完成。\n` +
1463
+ statsLine +
1464
+ `变更文件列表:\n${loopAgentChanges.join("\n")}` +
1465
+ (planFileRelPath ? `\n\n实施计划: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)` : "")
1466
+ );
1467
+ } else {
1468
+ updateChainContext(chainKey,
1469
+ `${step.loopAgentName} 已完成执行,未检测到文件变更。\n` +
1470
+ (planFileRelPath ? `实施计划: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)` : "")
1471
+ );
1472
+ }
1473
+
1241
1474
  if (isTimeoutResult(agentResult)) {
1242
1475
  if (mode === "full-auto") {
1243
- contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
1476
+ contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
1244
1477
  } else {
1245
1478
  const choice = await uiSelect(ctx, `⏰ ${step.loopAgentName} 执行超时`, [
1246
1479
  "1. 重新执行", "2. 进入审查阶段", "3. 跳过此步骤", "4. 取消工作流",
@@ -1248,20 +1481,20 @@ async function executeLoopGroup(
1248
1481
  if (!choice || choice.startsWith("4")) { cancelWorkflow(); return; }
1249
1482
  if (choice.startsWith("3")) { state.status = "skipped"; return; }
1250
1483
  if (choice.startsWith("2")) {
1251
- contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
1484
+ contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
1252
1485
  } else {
1253
1486
  agentResult = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, stepIndex, step.loopAgentName!, step.timeoutMs);
1254
1487
  if (isTimeoutResult(agentResult)) {
1255
- contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
1488
+ contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
1256
1489
  }
1257
1490
  }
1258
1491
  }
1259
1492
  }
1260
1493
 
1261
- // Run reviewer
1494
+ // ── Run reviewer ──
1262
1495
  const reviewTask = contextPrompt.includes("[TIMEOUT_WARNING]")
1263
1496
  ? contextPrompt
1264
- : buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd);
1497
+ : buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
1265
1498
 
1266
1499
  const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, reviewTimeoutMs);
1267
1500
 
@@ -1276,11 +1509,31 @@ async function executeLoopGroup(
1276
1509
  }
1277
1510
  }
1278
1511
 
1512
+ // ── After reviewer: capture review context for next loop ──
1513
+ // Use agent-specific key so worker-reviewer and trimmer-reviewer feedback don't interfere
1514
+ const reviewChainKey = step.loopAgentName === "worker" ? "代码审查反馈" : "精简审查反馈";
1515
+ if (reviewSummary) {
1516
+ const reviewCountParts: string[] = [];
1517
+ if (reviewSummary.critical > 0) reviewCountParts.push(`严重 ${reviewSummary.critical} 个`);
1518
+ if (reviewSummary.medium > 0) reviewCountParts.push(`中等 ${reviewSummary.medium} 个`);
1519
+ if (reviewSummary.low > 0) reviewCountParts.push(`低 ${reviewSummary.low} 个`);
1520
+ const reviewStats = reviewCountParts.length > 0 ? `发现 ${reviewCountParts.join(",")} 问题。` : "未发现问题。";
1521
+
1522
+ updateChainContext(reviewChainKey,
1523
+ `${reviewStats}\n` +
1524
+ `完整审查报告在 .pi-dev-output/pi-review/md/ 目录中,\n` +
1525
+ `请在工作流输出目录中 grep UUID "${_workflowId}" 查找最新报告。`
1526
+ );
1527
+ }
1528
+
1529
+ // ── Decide whether to loop ──
1279
1530
  if (reviewSummary?.maxSeverity === "critical" && loopCount < maxLoops) {
1531
+ const reviewReportHint = `\n完整审查报告在 .pi-dev-output/pi-review/md/(grep UUID "${_workflowId}" 查找)。`;
1280
1532
  if (mode === "full-auto") {
1281
1533
  contextPrompt = [prompt, "", "## 上次审查发现的问题",
1282
1534
  `审查摘要: ${JSON.stringify(reviewSummary)}`,
1283
1535
  `请修复 ${reviewSummary.critical} 个严重问题后重新运行。`,
1536
+ reviewReportHint,
1284
1537
  ].join("\n");
1285
1538
  continue;
1286
1539
  } else {
@@ -1290,6 +1543,7 @@ async function executeLoopGroup(
1290
1543
  contextPrompt = [prompt, "", "## 上次审查发现的问题",
1291
1544
  `审查摘要: ${JSON.stringify(reviewSummary)}`,
1292
1545
  `请修复这些严重问题后重新运行。`,
1546
+ reviewReportHint,
1293
1547
  ].join("\n");
1294
1548
  continue;
1295
1549
  }
@@ -1433,6 +1687,40 @@ async function executeWorkflowBackground(
1433
1687
  }
1434
1688
  }
1435
1689
 
1690
+ // ── Before executing docWriter, rebuild chain context with only relevant info ──
1691
+ if (step.agentName === "docWriter") {
1692
+ // Clear stale context (review feedback from worker/trimmer loops is noise for docWriter)
1693
+ resetChainContext();
1694
+ // Restore plan location
1695
+ if (planFileRelPathInner) {
1696
+ updateChainContext("实施计划",
1697
+ `计划文件: ${planFileRelPathInner}\n` +
1698
+ `(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)`
1699
+ );
1700
+ }
1701
+ // Build comprehensive change summary from ALL file changes
1702
+ const workerChanges = _workflowFileChanges
1703
+ .filter(c => c.type !== "read" && c.agent === "worker")
1704
+ .map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
1705
+ const trimmerChanges = _workflowFileChanges
1706
+ .filter(c => c.type !== "read" && c.agent === "trimmer")
1707
+ .map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
1708
+ const allChangeLines: string[] = [];
1709
+ if (workerChanges.length > 0) {
1710
+ allChangeLines.push(`🔧 Worker (代码实施) - ${workerChanges.length} 个文件:`);
1711
+ allChangeLines.push(...workerChanges);
1712
+ }
1713
+ if (trimmerChanges.length > 0) {
1714
+ allChangeLines.push(`✂️ Trimmer (代码精简) - ${trimmerChanges.length} 个文件:`);
1715
+ allChangeLines.push(...trimmerChanges);
1716
+ }
1717
+ if (allChangeLines.length > 0) {
1718
+ updateChainContext("本次工作流全部变更",
1719
+ `以下文件已被本次工作流修改:\n${allChangeLines.join("\n")}`
1720
+ );
1721
+ }
1722
+ }
1723
+
1436
1724
  // ── Execute (timer starts NOW, after all user confirmations) ──
1437
1725
  state.status = "running";
1438
1726
  const stepStartTime = Date.now();
@@ -1449,6 +1737,13 @@ async function executeWorkflowBackground(
1449
1737
  await executeSingleStep(ctx, step, state, agentMap, prompt, planFileRelPathInner, mode, currentStepIndex);
1450
1738
  if (step.agentName === "planner") {
1451
1739
  planFileRelPathInner = findLatestPlanFile(_workflowCwd);
1740
+ // Update chain context so subsequent agents know where the plan is
1741
+ if (planFileRelPathInner) {
1742
+ updateChainContext("实施计划",
1743
+ `计划文件: ${planFileRelPathInner}\n` +
1744
+ `(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)`
1745
+ );
1746
+ }
1452
1747
  }
1453
1748
  }
1454
1749
  state.status = "done";
@@ -1491,6 +1786,7 @@ async function executeWorkflowBackground(
1491
1786
  stepStates.every(s => s.status === "done" || s.status === "skipped") ? "done" : "failed",
1492
1787
  { toolCount: _widgetExtraToolCount, tokenCount: _widgetExtraTokenCount },
1493
1788
  taskSummary,
1789
+ _workflowId,
1494
1790
  );
1495
1791
  sendWorkflowResult(pi, finalState, prompt, _workflowType);
1496
1792
 
@@ -1504,6 +1800,7 @@ async function executeWorkflowBackground(
1504
1800
  function buildCp(): CheckpointData {
1505
1801
  return {
1506
1802
  version: 2,
1803
+ workflowId: _workflowId,
1507
1804
  createdAt: existingCp?.createdAt ?? new Date().toISOString(),
1508
1805
  updatedAt: new Date().toISOString(),
1509
1806
  prompt,
@@ -1622,6 +1919,12 @@ export async function runWorkflow(
1622
1919
  _workflowFileChanges = existingCp?.fileChanges ? [...existingCp.fileChanges] : [];
1623
1920
  _workflowAgentRunHistory = existingCp?.agentRunHistory ? [...existingCp.agentRunHistory] : [];
1624
1921
 
1922
+ // Initialize or restore workflow UUID
1923
+ _workflowId = existingCp?.workflowId ?? uuidv7();
1924
+
1925
+ // Reset chain context for new workflow session
1926
+ resetChainContext();
1927
+
1625
1928
  // ── Baseline snapshot: restore from checkpoint or capture fresh ──
1626
1929
  if (resumeFlow && existingCp?.baseline) {
1627
1930
  _workflowBaseline = [...existingCp.baseline];
@@ -1631,6 +1934,7 @@ export async function runWorkflow(
1631
1934
 
1632
1935
  saveCheckpoint(ctx.cwd, {
1633
1936
  version: 2,
1937
+ workflowId: _workflowId,
1634
1938
  createdAt: existingCp?.createdAt ?? new Date().toISOString(),
1635
1939
  updatedAt: new Date().toISOString(),
1636
1940
  prompt,
@@ -1673,13 +1977,16 @@ export async function runWorkflow(
1673
1977
  _widgetCurrentIdx,
1674
1978
  _widgetStartTime,
1675
1979
  "cancelled",
1980
+ undefined,
1676
1981
  extractTaskSummary(_workflowPrompt),
1982
+ _workflowId,
1677
1983
  );
1678
1984
  updateWorkflowWidget(_lastWorkflowCtx, finalState);
1679
1985
 
1680
1986
  // ── Save final checkpoint before archiving ──
1681
1987
  const cancelCp: CheckpointData = {
1682
1988
  version: 2,
1989
+ workflowId: _workflowId,
1683
1990
  createdAt: _workflowCreatedAt,
1684
1991
  updatedAt: new Date().toISOString(),
1685
1992
  prompt: _workflowPrompt,
@@ -1730,7 +2037,7 @@ export async function runWorkflow(
1730
2037
  if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
1731
2038
  const now = Date.now();
1732
2039
  if (_lastEscPressTime > 0 && now - _lastEscPressTime < 3000) {
1733
- // Second Esc press within 5s → confirm cancel
2040
+ // Second Esc press within 3s → confirm cancel
1734
2041
  ctx.ui.notify("⏹️ 正在停止工作流...", "warning");
1735
2042
  cancelWorkflow();
1736
2043
  _lastEscPressTime = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghyper9023/pi-dev-workflow",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "keywords": [
5
5
  "pi-package"
6
6
  ],