@ghyper9023/pi-dev-workflow 0.4.3 → 0.5.1

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 (61) hide show
  1. package/.doc/AGENT-FRONTMATTER-REFERENCE.md +198 -0
  2. package/.version/RELEASE-v0.5.0.md +82 -0
  3. package/README.md +2 -0
  4. package/agents/git-agent.md +7 -0
  5. package/agents/grill/dev-doc-grill-agent.md +7 -0
  6. package/agents/grill/dev-fix-grill-agent.md +7 -0
  7. package/agents/grill/dev-grill-agent.md +7 -0
  8. package/agents/grill/dev-perf-grill-agent.md +7 -0
  9. package/agents/grill/dev-prd-agent.md +7 -0
  10. package/agents/grill/dev-refactor-grill-agent.md +7 -0
  11. package/agents/grill/dev-test-grill-agent.md +7 -0
  12. package/agents/review-agent.md +7 -0
  13. package/agents/workflow/docWriter-agent.md +13 -2
  14. package/agents/workflow/planner-agent.md +16 -3
  15. package/agents/workflow/reviewer-agent.md +21 -4
  16. package/agents/workflow/trimmer-agent.md +12 -1
  17. package/agents/workflow/worker-agent.md +17 -2
  18. package/extensions/sub-agents.ts +192 -30
  19. package/extensions/ui-helpers.ts +20 -20
  20. package/extensions/workflow-engine.ts +347 -16
  21. package/package.json +1 -1
  22. package/tests/test-workflow-engine-bugs.mjs +317 -0
  23. package/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +0 -14
  24. package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +0 -58
  25. package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +0 -13
  26. package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +0 -13
  27. package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +0 -13
  28. package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +0 -13
  29. package/.pi-dev-output/pi-grill/answers/answer-mpfttme1-20260522-0230.md +0 -13
  30. package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +0 -94
  31. package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +0 -150
  32. package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +0 -215
  33. package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +0 -240
  34. package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +0 -253
  35. package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +0 -137
  36. package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +0 -258
  37. package/.pi-dev-output/pi-plans/20260522-113000-grill-left-arrow-fix.md +0 -274
  38. package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +0 -196
  39. package/.pi-dev-output/pi-review/md/review-20260520-100000.md +0 -91
  40. package/.pi-dev-output/pi-review/md/review-20260521-140000.md +0 -191
  41. package/.pi-dev-output/pi-review/md/review-20260521-190000.md +0 -189
  42. package/.pi-dev-output/pi-review/md/review-20260521-204500.md +0 -241
  43. package/.pi-dev-output/pi-review/md/review-20260521-214500.md +0 -270
  44. package/.pi-dev-output/pi-review/md/review-20260521-215158.md +0 -214
  45. package/.pi-dev-output/pi-review/md/review-20260521-234500.md +0 -201
  46. package/.pi-dev-output/pi-review/md/review-20260521-235500.md +0 -422
  47. package/.pi-dev-output/pi-review/md/review-20260522-000000.md +0 -212
  48. package/.pi-dev-output/pi-review/md/review-20260522-003000.md +0 -377
  49. package/.pi-dev-output/pi-review/md/review-20260522-003500.md +0 -296
  50. package/.pi-dev-output/pi-review/md/review-20260522-105000.md +0 -166
  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-20260522-113000-grill-left-arrow-fix.json +0 -473
  58. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +0 -30
  59. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +0 -49
  60. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +0 -59
  61. 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,43 @@ 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
+ }
1381
+
1382
+ // ── Capture AI work summary as supplementary chain context ──
1383
+ const workSummary = extractFinalOutput(result.output);
1384
+ if (workSummary) {
1385
+ updateChainContext(`${agentName} 工作总结`,
1386
+ `${agentName} 已完成工作,以下是其工作总结:\n\n${workSummary}`
1387
+ );
1388
+ }
1179
1389
  }
1180
1390
 
1181
1391
  // ═══════════════════════════════════════════════════════════════
@@ -1213,13 +1423,13 @@ async function executeLoopGroup(
1213
1423
  startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
1214
1424
  });
1215
1425
 
1216
- // 每次循环开始时重置 sub-step 状态
1217
- setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
1218
- setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
1426
+ // 每次循环开始时重置 sub-step 状态(清除上一轮的计时和工具记录)
1427
+ resetWidgetSubStepTimers(stepIndex, step.loopAgentName!);
1428
+ resetWidgetSubStepTimers(stepIndex, step.reviewAgentName!);
1219
1429
  const loopStartTime = Date.now();
1220
1430
 
1221
- // Run loop agent
1222
- const loopTask = buildTaskForStep(step.loopAgentName!, contextPrompt, planFileRelPath, _workflowCwd);
1431
+ // ── Run loop agent (worker / trimmer) ──
1432
+ const loopTask = buildTaskForStep(step.loopAgentName!, contextPrompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
1223
1433
 
1224
1434
  let agentResult = await runAgentWithProgress(loopAgent, loopTask, stepIndex, step.loopAgentName!, step.timeoutMs);
1225
1435
 
@@ -1238,9 +1448,48 @@ async function executeLoopGroup(
1238
1448
  }
1239
1449
  }
1240
1450
 
1451
+ // ── After loop agent completes: capture changes + stats for chain context ──
1452
+ const loopAgentChanges = _workflowFileChanges
1453
+ .filter(c => c.stepIndex === stepIndex && c.agent === step.loopAgentName)
1454
+ .map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
1455
+
1456
+ // Use agent-specific keys so worker and trimmer changes coexist without overwriting
1457
+ const chainKey = step.loopAgentName === "worker" ? "代码实施摘要" : "代码精简摘要";
1458
+
1459
+ if (loopAgentChanges.length > 0) {
1460
+ const editCount = loopAgentChanges.filter(c => c.startsWith("M")).length;
1461
+ const newCount = loopAgentChanges.filter(c => c.startsWith("A")).length;
1462
+ const delCount = loopAgentChanges.filter(c => c.startsWith("D")).length;
1463
+ const statsParts: string[] = [];
1464
+ if (editCount > 0) statsParts.push(`修改 ${editCount} 个`);
1465
+ if (newCount > 0) statsParts.push(`新增 ${newCount} 个`);
1466
+ if (delCount > 0) statsParts.push(`删除 ${delCount} 个`);
1467
+ const statsLine = statsParts.length > 0 ? `改动统计: ${statsParts.join(",")}\n\n` : "";
1468
+
1469
+ updateChainContext(chainKey,
1470
+ `${step.loopAgentName === "worker" ? "代码实施" : "代码精简"}已完成。\n` +
1471
+ statsLine +
1472
+ `变更文件列表:\n${loopAgentChanges.join("\n")}` +
1473
+ (planFileRelPath ? `\n\n实施计划: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)` : "")
1474
+ );
1475
+ } else {
1476
+ updateChainContext(chainKey,
1477
+ `${step.loopAgentName} 已完成执行,未检测到文件变更。\n` +
1478
+ (planFileRelPath ? `实施计划: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)` : "")
1479
+ );
1480
+ }
1481
+
1482
+ // ── Capture loop agent work summary as supplementary chain context ──
1483
+ const loopFinalText = extractFinalOutput(agentResult.output);
1484
+ if (loopFinalText) {
1485
+ updateChainContext(`${step.loopAgentName} 工作总结`,
1486
+ `${step.loopAgentName} 已完成工作,以下是其工作总结:\n\n${loopFinalText}`
1487
+ );
1488
+ }
1489
+
1241
1490
  if (isTimeoutResult(agentResult)) {
1242
1491
  if (mode === "full-auto") {
1243
- contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
1492
+ contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
1244
1493
  } else {
1245
1494
  const choice = await uiSelect(ctx, `⏰ ${step.loopAgentName} 执行超时`, [
1246
1495
  "1. 重新执行", "2. 进入审查阶段", "3. 跳过此步骤", "4. 取消工作流",
@@ -1248,20 +1497,20 @@ async function executeLoopGroup(
1248
1497
  if (!choice || choice.startsWith("4")) { cancelWorkflow(); return; }
1249
1498
  if (choice.startsWith("3")) { state.status = "skipped"; return; }
1250
1499
  if (choice.startsWith("2")) {
1251
- contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
1500
+ contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
1252
1501
  } else {
1253
1502
  agentResult = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, stepIndex, step.loopAgentName!, step.timeoutMs);
1254
1503
  if (isTimeoutResult(agentResult)) {
1255
- contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
1504
+ contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
1256
1505
  }
1257
1506
  }
1258
1507
  }
1259
1508
  }
1260
1509
 
1261
- // Run reviewer
1510
+ // ── Run reviewer ──
1262
1511
  const reviewTask = contextPrompt.includes("[TIMEOUT_WARNING]")
1263
1512
  ? contextPrompt
1264
- : buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd);
1513
+ : buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
1265
1514
 
1266
1515
  const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, reviewTimeoutMs);
1267
1516
 
@@ -1276,11 +1525,39 @@ async function executeLoopGroup(
1276
1525
  }
1277
1526
  }
1278
1527
 
1528
+ // ── After reviewer: capture review context for next loop ──
1529
+ // Use agent-specific key so worker-reviewer and trimmer-reviewer feedback don't interfere
1530
+ const reviewChainKey = step.loopAgentName === "worker" ? "代码审查反馈" : "精简审查反馈";
1531
+ if (reviewSummary) {
1532
+ const reviewCountParts: string[] = [];
1533
+ if (reviewSummary.critical > 0) reviewCountParts.push(`严重 ${reviewSummary.critical} 个`);
1534
+ if (reviewSummary.medium > 0) reviewCountParts.push(`中等 ${reviewSummary.medium} 个`);
1535
+ if (reviewSummary.low > 0) reviewCountParts.push(`低 ${reviewSummary.low} 个`);
1536
+ const reviewStats = reviewCountParts.length > 0 ? `发现 ${reviewCountParts.join(",")} 问题。` : "未发现问题。";
1537
+
1538
+ updateChainContext(reviewChainKey,
1539
+ `${reviewStats}\n` +
1540
+ `完整审查报告在 .pi-dev-output/pi-review/md/ 目录中,\n` +
1541
+ `请在工作流输出目录中 grep UUID "${_workflowId}" 查找最新报告。`
1542
+ );
1543
+ }
1544
+
1545
+ // ── Capture reviewer work summary as supplementary chain context ──
1546
+ // Reuse already-parsed extractedOutput to avoid double parsing
1547
+ if (extractedOutput) {
1548
+ updateChainContext(`审查工作总结`,
1549
+ `审查者已完成审查,以下是其审查总结:\n\n${extractedOutput}`
1550
+ );
1551
+ }
1552
+
1553
+ // ── Decide whether to loop ──
1279
1554
  if (reviewSummary?.maxSeverity === "critical" && loopCount < maxLoops) {
1555
+ const reviewReportHint = `\n完整审查报告在 .pi-dev-output/pi-review/md/(grep UUID "${_workflowId}" 查找)。`;
1280
1556
  if (mode === "full-auto") {
1281
1557
  contextPrompt = [prompt, "", "## 上次审查发现的问题",
1282
1558
  `审查摘要: ${JSON.stringify(reviewSummary)}`,
1283
1559
  `请修复 ${reviewSummary.critical} 个严重问题后重新运行。`,
1560
+ reviewReportHint,
1284
1561
  ].join("\n");
1285
1562
  continue;
1286
1563
  } else {
@@ -1290,6 +1567,7 @@ async function executeLoopGroup(
1290
1567
  contextPrompt = [prompt, "", "## 上次审查发现的问题",
1291
1568
  `审查摘要: ${JSON.stringify(reviewSummary)}`,
1292
1569
  `请修复这些严重问题后重新运行。`,
1570
+ reviewReportHint,
1293
1571
  ].join("\n");
1294
1572
  continue;
1295
1573
  }
@@ -1433,6 +1711,40 @@ async function executeWorkflowBackground(
1433
1711
  }
1434
1712
  }
1435
1713
 
1714
+ // ── Before executing docWriter, rebuild chain context with only relevant info ──
1715
+ if (step.agentName === "docWriter") {
1716
+ // Clear stale context (review feedback from worker/trimmer loops is noise for docWriter)
1717
+ resetChainContext();
1718
+ // Restore plan location
1719
+ if (planFileRelPathInner) {
1720
+ updateChainContext("实施计划",
1721
+ `计划文件: ${planFileRelPathInner}\n` +
1722
+ `(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)`
1723
+ );
1724
+ }
1725
+ // Build comprehensive change summary from ALL file changes
1726
+ const workerChanges = _workflowFileChanges
1727
+ .filter(c => c.type !== "read" && c.agent === "worker")
1728
+ .map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
1729
+ const trimmerChanges = _workflowFileChanges
1730
+ .filter(c => c.type !== "read" && c.agent === "trimmer")
1731
+ .map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
1732
+ const allChangeLines: string[] = [];
1733
+ if (workerChanges.length > 0) {
1734
+ allChangeLines.push(`🔧 Worker (代码实施) - ${workerChanges.length} 个文件:`);
1735
+ allChangeLines.push(...workerChanges);
1736
+ }
1737
+ if (trimmerChanges.length > 0) {
1738
+ allChangeLines.push(`✂️ Trimmer (代码精简) - ${trimmerChanges.length} 个文件:`);
1739
+ allChangeLines.push(...trimmerChanges);
1740
+ }
1741
+ if (allChangeLines.length > 0) {
1742
+ updateChainContext("本次工作流全部变更",
1743
+ `以下文件已被本次工作流修改:\n${allChangeLines.join("\n")}`
1744
+ );
1745
+ }
1746
+ }
1747
+
1436
1748
  // ── Execute (timer starts NOW, after all user confirmations) ──
1437
1749
  state.status = "running";
1438
1750
  const stepStartTime = Date.now();
@@ -1449,6 +1761,13 @@ async function executeWorkflowBackground(
1449
1761
  await executeSingleStep(ctx, step, state, agentMap, prompt, planFileRelPathInner, mode, currentStepIndex);
1450
1762
  if (step.agentName === "planner") {
1451
1763
  planFileRelPathInner = findLatestPlanFile(_workflowCwd);
1764
+ // Update chain context so subsequent agents know where the plan is
1765
+ if (planFileRelPathInner) {
1766
+ updateChainContext("实施计划",
1767
+ `计划文件: ${planFileRelPathInner}\n` +
1768
+ `(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)`
1769
+ );
1770
+ }
1452
1771
  }
1453
1772
  }
1454
1773
  state.status = "done";
@@ -1491,6 +1810,7 @@ async function executeWorkflowBackground(
1491
1810
  stepStates.every(s => s.status === "done" || s.status === "skipped") ? "done" : "failed",
1492
1811
  { toolCount: _widgetExtraToolCount, tokenCount: _widgetExtraTokenCount },
1493
1812
  taskSummary,
1813
+ _workflowId,
1494
1814
  );
1495
1815
  sendWorkflowResult(pi, finalState, prompt, _workflowType);
1496
1816
 
@@ -1504,6 +1824,7 @@ async function executeWorkflowBackground(
1504
1824
  function buildCp(): CheckpointData {
1505
1825
  return {
1506
1826
  version: 2,
1827
+ workflowId: _workflowId,
1507
1828
  createdAt: existingCp?.createdAt ?? new Date().toISOString(),
1508
1829
  updatedAt: new Date().toISOString(),
1509
1830
  prompt,
@@ -1622,6 +1943,12 @@ export async function runWorkflow(
1622
1943
  _workflowFileChanges = existingCp?.fileChanges ? [...existingCp.fileChanges] : [];
1623
1944
  _workflowAgentRunHistory = existingCp?.agentRunHistory ? [...existingCp.agentRunHistory] : [];
1624
1945
 
1946
+ // Initialize or restore workflow UUID
1947
+ _workflowId = existingCp?.workflowId ?? uuidv7();
1948
+
1949
+ // Reset chain context for new workflow session
1950
+ resetChainContext();
1951
+
1625
1952
  // ── Baseline snapshot: restore from checkpoint or capture fresh ──
1626
1953
  if (resumeFlow && existingCp?.baseline) {
1627
1954
  _workflowBaseline = [...existingCp.baseline];
@@ -1631,6 +1958,7 @@ export async function runWorkflow(
1631
1958
 
1632
1959
  saveCheckpoint(ctx.cwd, {
1633
1960
  version: 2,
1961
+ workflowId: _workflowId,
1634
1962
  createdAt: existingCp?.createdAt ?? new Date().toISOString(),
1635
1963
  updatedAt: new Date().toISOString(),
1636
1964
  prompt,
@@ -1673,13 +2001,16 @@ export async function runWorkflow(
1673
2001
  _widgetCurrentIdx,
1674
2002
  _widgetStartTime,
1675
2003
  "cancelled",
2004
+ undefined,
1676
2005
  extractTaskSummary(_workflowPrompt),
2006
+ _workflowId,
1677
2007
  );
1678
2008
  updateWorkflowWidget(_lastWorkflowCtx, finalState);
1679
2009
 
1680
2010
  // ── Save final checkpoint before archiving ──
1681
2011
  const cancelCp: CheckpointData = {
1682
2012
  version: 2,
2013
+ workflowId: _workflowId,
1683
2014
  createdAt: _workflowCreatedAt,
1684
2015
  updatedAt: new Date().toISOString(),
1685
2016
  prompt: _workflowPrompt,
@@ -1730,7 +2061,7 @@ export async function runWorkflow(
1730
2061
  if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
1731
2062
  const now = Date.now();
1732
2063
  if (_lastEscPressTime > 0 && now - _lastEscPressTime < 3000) {
1733
- // Second Esc press within 5s → confirm cancel
2064
+ // Second Esc press within 3s → confirm cancel
1734
2065
  ctx.ui.notify("⏹️ 正在停止工作流...", "warning");
1735
2066
  cancelWorkflow();
1736
2067
  _lastEscPressTime = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghyper9023/pi-dev-workflow",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "keywords": [
5
5
  "pi-package"
6
6
  ],