@ghyper9023/pi-dev-workflow 0.3.3 → 0.4.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.
@@ -26,16 +26,20 @@
26
26
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
27
27
  import { runGrillPhase, runPRDPhase, saveAnswerFile, recoverFromBackup, type GrillOptions } from "./grill-me-agent";
28
28
  import { discoverAgents } from "./sub-agents";
29
+ import { runWorkflow, loadCheckpointFromFile, type WorkflowStepDef } from "./workflow-engine";
30
+ import { uiSelect, uiConfirm, uiInput, BACK_MARKER } from "./ui-helpers";
29
31
 
30
32
  // ── Helpers ──────────────────────────────────────────────────
31
33
 
32
- /** Ask a single question. Returns `undefined` on cancel (Esc). */
34
+ /** Ask a single question with proper wrapping. Returns `undefined` on cancel (Esc), or BACK_MARKER for back. */
33
35
  async function ask(
34
36
  ctx: ExtensionCommandContext,
35
37
  label: string,
36
38
  placeholder: string,
39
+ backable = false,
40
+ initialValue = "",
37
41
  ): Promise<string | undefined> {
38
- return ctx.ui.input(label, { placeholder, required: false });
42
+ return uiInput(ctx, label, placeholder, false, backable, initialValue);
39
43
  }
40
44
 
41
45
  /** Check if a field value is empty or explicitly "无". */
@@ -360,12 +364,276 @@ const _refactorGrillAgent = discoverAgents().find(a => a.name === "dev-refactor-
360
364
  const _testGrillAgent = discoverAgents().find(a => a.name === "dev-test-grill-agent")!;
361
365
  const _perfGrillAgent = discoverAgents().find(a => a.name === "dev-perf-grill-agent")!;
362
366
 
367
+ // ── Workflow configurations ──────────────────────────────────
368
+
369
+ const FEAT_WORKFLOW_STEPS: WorkflowStepDef[] = [
370
+ {
371
+ id: "planner",
372
+ label: "📋 生成实施计划",
373
+ type: "auto",
374
+ agentName: "planner",
375
+ timeoutMs: 900_000,
376
+ },
377
+ {
378
+ id: "worker-reviewer",
379
+ label: "🔧 实施代码 → 审查",
380
+ type: "loop-group",
381
+ loopAgentName: "worker",
382
+ reviewAgentName: "reviewer",
383
+ maxLoops: 3,
384
+ timeoutMs: 900_000,
385
+ },
386
+ {
387
+ id: "trimmer-reviewer",
388
+ label: "✂️ 精简代码 → 审查",
389
+ type: "loop-group",
390
+ loopAgentName: "trimmer",
391
+ reviewAgentName: "reviewer",
392
+ maxLoops: 3,
393
+ timeoutMs: 300_000,
394
+ },
395
+ {
396
+ id: "docWriter",
397
+ label: "📝 更新文档",
398
+ type: "confirm",
399
+ agentName: "docWriter",
400
+ timeoutMs: 300_000,
401
+ },
402
+ ];
403
+
404
+ const FIX_WORKFLOW_STEPS: WorkflowStepDef[] = [
405
+ {
406
+ id: "planner",
407
+ label: "📋 分析根因并制定修复计划",
408
+ type: "auto",
409
+ agentName: "planner",
410
+ timeoutMs: 900_000,
411
+ },
412
+ {
413
+ id: "worker-reviewer",
414
+ label: "🔧 修复代码 → 审查",
415
+ type: "loop-group",
416
+ loopAgentName: "worker",
417
+ reviewAgentName: "reviewer",
418
+ maxLoops: 3,
419
+ timeoutMs: 900_000,
420
+ },
421
+ {
422
+ id: "docWriter",
423
+ label: "📝 更新文档",
424
+ type: "confirm",
425
+ agentName: "docWriter",
426
+ timeoutMs: 300_000,
427
+ },
428
+ ];
429
+
430
+ const REFACTOR_WORKFLOW_STEPS: WorkflowStepDef[] = [
431
+ {
432
+ id: "planner",
433
+ label: "📋 分析重构计划",
434
+ type: "auto",
435
+ agentName: "planner",
436
+ timeoutMs: 900_000,
437
+ },
438
+ {
439
+ id: "worker-reviewer",
440
+ label: "🔧 重构代码 → 审查",
441
+ type: "loop-group",
442
+ loopAgentName: "worker",
443
+ reviewAgentName: "reviewer",
444
+ maxLoops: 3,
445
+ timeoutMs: 900_000,
446
+ },
447
+ {
448
+ id: "trimmer-reviewer",
449
+ label: "✂️ 精简代码 → 审查",
450
+ type: "loop-group",
451
+ loopAgentName: "trimmer",
452
+ reviewAgentName: "reviewer",
453
+ maxLoops: 3,
454
+ timeoutMs: 300_000,
455
+ },
456
+ ];
457
+
458
+ const PERF_WORKFLOW_STEPS: WorkflowStepDef[] = [
459
+ {
460
+ id: "planner",
461
+ label: "📋 分析性能问题并制定优化计划",
462
+ type: "auto",
463
+ agentName: "planner",
464
+ timeoutMs: 900_000,
465
+ },
466
+ {
467
+ id: "worker-reviewer",
468
+ label: "⚡ 优化代码 → 审查",
469
+ type: "loop-group",
470
+ loopAgentName: "worker",
471
+ reviewAgentName: "reviewer",
472
+ maxLoops: 3,
473
+ timeoutMs: 900_000,
474
+ },
475
+ ];
476
+
477
+ const TEST_WORKFLOW_STEPS: WorkflowStepDef[] = [
478
+ {
479
+ id: "planner",
480
+ label: "📋 分析测试计划",
481
+ type: "auto",
482
+ agentName: "planner",
483
+ timeoutMs: 900_000,
484
+ },
485
+ {
486
+ id: "worker-reviewer",
487
+ label: "🧪 编写测试 → 审查",
488
+ type: "loop-group",
489
+ loopAgentName: "worker",
490
+ reviewAgentName: "reviewer",
491
+ maxLoops: 3,
492
+ timeoutMs: 900_000,
493
+ },
494
+ ];
495
+
496
+ const DOC_WORKFLOW_STEPS: WorkflowStepDef[] = [
497
+ {
498
+ id: "planner",
499
+ label: "📋 分析文档需求",
500
+ type: "auto",
501
+ agentName: "planner",
502
+ timeoutMs: 900_000,
503
+ },
504
+ {
505
+ id: "docWriter",
506
+ label: "📝 撰写文档",
507
+ type: "auto",
508
+ agentName: "docWriter",
509
+ timeoutMs: 300_000,
510
+ },
511
+ ];
512
+
513
+ const STYLE_WORKFLOW_STEPS: WorkflowStepDef[] = [
514
+ {
515
+ id: "trimmer-reviewer",
516
+ label: "✂️ 风格调整 → 审查",
517
+ type: "loop-group",
518
+ loopAgentName: "trimmer",
519
+ reviewAgentName: "reviewer",
520
+ maxLoops: 2,
521
+ timeoutMs: 300_000,
522
+ },
523
+ ];
524
+
525
+ const SECURITY_WORKFLOW_STEPS: WorkflowStepDef[] = [
526
+ {
527
+ id: "reviewer",
528
+ label: "🔒 安全审查",
529
+ type: "auto",
530
+ agentName: "reviewer",
531
+ timeoutMs: 300_000,
532
+ },
533
+ ];
534
+
363
535
  // ── Command runner ───────────────────────────────────────────
364
536
 
537
+ /** Format workflow steps into a readable list. */
538
+ function formatWorkflowSteps(steps: WorkflowStepDef[]): string {
539
+ return steps.map((s, i) => `${i + 1}. ${s.label}`).join("\n");
540
+ }
541
+
365
542
  /**
366
- * Run a wizard with an optional Grill phase:
367
- * Wizard → Assemble → (Grill?) → Send
543
+ * Prompt user to choose workflow mode and optionally customize sub-agent chain.
544
+ *
545
+ * Returns true if workflow was started and handled (caller should return immediately).
546
+ * Returns false if caller should fall through to direct prompt sending.
368
547
  */
548
+ async function promptWorkflowDecision(
549
+ ctx: ExtensionCommandContext,
550
+ pi: ExtensionAPI,
551
+ finalPrompt: string,
552
+ defaultSteps: WorkflowStepDef[],
553
+ ): Promise<boolean> {
554
+ if (!defaultSteps || defaultSteps.length === 0) return false;
555
+
556
+ const choice = await uiSelect(
557
+ ctx,
558
+ "🚀 选择工作流模式",
559
+ [
560
+ "1. 使用默认链式子代理(推荐)",
561
+ "2. 自定义链式子代理",
562
+ "3. 退出工作流(直接发送 prompt 给主代理)",
563
+ ],
564
+ );
565
+
566
+ if (!choice || choice.startsWith("3")) {
567
+ return false;
568
+ }
569
+
570
+ if (choice.startsWith("1")) {
571
+ saveAnswerFile(ctx.cwd, finalPrompt);
572
+ await runWorkflow(ctx, pi, finalPrompt, { steps: defaultSteps }, "快速链式");
573
+ return true;
574
+ }
575
+
576
+ // ── Custom mode: let user pick steps and set timeouts (with back support) ──
577
+ const customSteps: WorkflowStepDef[] = [];
578
+ let stepIdx = 0;
579
+ while (stepIdx >= 0 && stepIdx < defaultSteps.length) {
580
+ const step = defaultSteps[stepIdx]!;
581
+
582
+ // If this step was already added, show current config, else show default
583
+ const existingStep = customSteps.find(cs => cs.id === step.id);
584
+ const include = await uiConfirm(
585
+ ctx,
586
+ `📌 ${step.label}${existingStep ? ' (已添加)' : ''}`,
587
+ `类型: ${step.type}\n默认超时: ${(step.timeoutMs / 60000).toFixed(0)} 分钟`,
588
+ stepIdx > 0, // backable only after first step
589
+ );
590
+ if (include === "back") {
591
+ // Go back: remove the last added step
592
+ if (customSteps.length > 0) {
593
+ customSteps.pop();
594
+ stepIdx--;
595
+ continue;
596
+ }
597
+ return false; // nothing to go back to
598
+ }
599
+ if (include === undefined) return false; // Esc
600
+
601
+ if (!include) {
602
+ stepIdx++;
603
+ continue; // skip this step
604
+ }
605
+
606
+ const existingTimeout = existingStep?.timeoutMs;
607
+ const timeoutStr = await uiInput(
608
+ ctx,
609
+ `⏱️ ${step.label} - 超时时间(分钟)`,
610
+ existingTimeout
611
+ ? `当前: ${(existingTimeout / 60000).toFixed(0)} 分钟,留空保持`
612
+ : `留空保持默认 (${(step.timeoutMs / 60000).toFixed(0)} 分钟)`,
613
+ );
614
+ if (timeoutStr === undefined) { stepIdx++; continue; } // Esc on input = skip
615
+
616
+ if (existingStep) {
617
+ // Update existing step's timeout
618
+ existingStep.timeoutMs = timeoutStr ? parseInt(timeoutStr, 10) * 60 * 1000 || step.timeoutMs : step.timeoutMs;
619
+ } else {
620
+ customSteps.push({
621
+ ...step,
622
+ timeoutMs: timeoutStr ? parseInt(timeoutStr, 10) * 60 * 1000 || step.timeoutMs : step.timeoutMs,
623
+ });
624
+ }
625
+ stepIdx++;
626
+ }
627
+
628
+ if (customSteps.length === 0) {
629
+ return false;
630
+ }
631
+
632
+ saveAnswerFile(ctx.cwd, finalPrompt);
633
+ await runWorkflow(ctx, pi, finalPrompt, { steps: customSteps }, "自定义");
634
+ return true;
635
+ }
636
+
369
637
  async function runWizardWithGrill(
370
638
  ctx: ExtensionCommandContext,
371
639
  pi: ExtensionAPI,
@@ -374,17 +642,30 @@ async function runWizardWithGrill(
374
642
  questions: Array<{ label: string; placeholder: string; key: string }>,
375
643
  assembler: (answers: Record<string, string>) => string,
376
644
  grillOptions?: GrillOptions,
645
+ workflowConfig?: { steps: WorkflowStepDef[] },
377
646
  ): Promise<void> {
378
- ctx.ui.notify(`📋 /dev-${type} — ${label},请逐项填写以下信息(留空跳过对应段落,Esc 取消)`, "info");
379
-
380
647
  const answers: Record<string, string> = {};
381
- for (const q of questions) {
382
- const val = await ask(ctx, q.label, q.placeholder);
648
+ let idx = 0;
649
+
650
+ while (idx >= 0 && idx < questions.length) {
651
+ const q = questions[idx]!;
652
+ const existingVal = answers[q.key];
653
+ const placeholder = existingVal
654
+ ? `(之前: ${existingVal.slice(0, 60)}) ${q.placeholder}`
655
+ : q.placeholder;
656
+ const val = await ask(ctx, q.label, placeholder, true, existingVal || "");
383
657
  if (val === undefined) {
384
- ctx.ui.notify("❌ 已取消", "warning");
658
+ return;
659
+ }
660
+ if (val === BACK_MARKER) {
661
+ if (idx > 0) {
662
+ idx--;
663
+ continue;
664
+ }
385
665
  return;
386
666
  }
387
667
  answers[q.key] = val;
668
+ idx++;
388
669
  }
389
670
 
390
671
  const basePrompt = assembler(answers);
@@ -400,39 +681,32 @@ async function runWizardWithGrill(
400
681
  loaderLabel: grillOptions.loaderLabel,
401
682
  });
402
683
  if (grillResult.cancelled) {
403
- ctx.ui.notify("❌ 操作已取消", "warning");
404
684
  return;
405
685
  }
406
686
  finalPrompt = grillResult.enhancedPrompt;
407
687
  }
408
688
 
409
- // ── Guard & persist before sending ───────────────────────
689
+ // ── Workflow phase ───────────────────────────
690
+ if (workflowConfig && workflowConfig.steps.length > 0) {
691
+ const handled = await promptWorkflowDecision(ctx, pi, finalPrompt, workflowConfig.steps);
692
+ if (handled) return;
693
+ }
694
+
695
+ // ── Guard & persist before sending ──────────────────────
410
696
  if (!finalPrompt) {
411
- ctx.ui.notify("⚠️ 最终提示词为空,正在尝试从备份文件恢复...", "warning");
412
697
  const recovered = recoverFromBackup(ctx.cwd);
413
698
  if (recovered) {
414
699
  finalPrompt = recovered;
415
- ctx.ui.notify("✅ 已从备份文件恢复提示词内容", "success");
416
700
  } else {
417
- ctx.ui.notify("❌ 错误:最终提示词为空且无可用备份,无法发送", "error");
418
701
  return;
419
702
  }
420
703
  }
421
704
  const answerPath = saveAnswerFile(ctx.cwd, finalPrompt);
422
-
423
- ctx.ui.notify(`✅ 提示词已组装完成,正在发送给主代理...`, "success");
424
705
  pi.sendUserMessage(finalPrompt, { deliverAs: "followUp" });
425
- ctx.ui.notify(`📝 /dev-${type} 提示词已投递,主代理正在处理。备份: ${answerPath}`, "info");
426
706
  }
427
707
 
428
708
  /**
429
709
  * Run a wizard: ask questions, assemble prompt, send to agent.
430
- * @param ctx Command context
431
- * @param pi Extension API (for sendUserMessage)
432
- * @param type Display name of the prompt type (e.g. "feat")
433
- * @param label Friendly label (e.g. "新功能/创意生成")
434
- * @param questions Array of [label, placeholder] pairs
435
- * @param assembler Function that takes answers and returns the prompt string
436
710
  */
437
711
  async function runWizard(
438
712
  ctx: ExtensionCommandContext,
@@ -441,28 +715,48 @@ async function runWizard(
441
715
  label: string,
442
716
  questions: Array<{ label: string; placeholder: string; key: string }>,
443
717
  assembler: (answers: Record<string, string>) => string,
718
+ workflowConfig?: { steps: WorkflowStepDef[] },
444
719
  ): Promise<void> {
445
- ctx.ui.notify(`📋 /dev-${type} — ${label},请逐项填写以下信息(留空跳过对应段落,Esc 取消)`, "info");
446
-
447
720
  const answers: Record<string, string> = {};
448
-
449
- for (const q of questions) {
450
- const val = await ask(ctx, q.label, q.placeholder);
721
+ let idx = 0;
722
+
723
+ while (idx >= 0 && idx < questions.length) {
724
+ const q = questions[idx]!;
725
+ const existingVal = answers[q.key];
726
+ const placeholder = existingVal
727
+ ? `(之前: ${existingVal.slice(0, 60)}) ${q.placeholder}`
728
+ : q.placeholder;
729
+ const val = await ask(ctx, q.label, placeholder, true, existingVal || "");
451
730
  if (val === undefined) {
452
- ctx.ui.notify("❌ 已取消", "warning");
731
+ // Esc → cancel whole wizard
732
+ return;
733
+ }
734
+ if (val === BACK_MARKER) {
735
+ // Go back to previous question
736
+ if (idx > 0) {
737
+ idx--;
738
+ continue;
739
+ }
740
+ // Already at first question → cancel
453
741
  return;
454
742
  }
455
743
  answers[q.key] = val;
744
+ idx++;
456
745
  }
457
746
 
458
747
  const prompt = assembler(answers);
459
- ctx.ui.notify(`✅ 提示词已组装完成,正在发送给主代理...`, "success");
748
+
749
+ // ── Workflow phase ───────────────────────────
750
+ if (workflowConfig && workflowConfig.steps.length > 0) {
751
+ const handled = await promptWorkflowDecision(ctx, pi, prompt, workflowConfig.steps);
752
+ if (handled) return;
753
+ }
754
+
755
+ // Persist prompt before sending
756
+ saveAnswerFile(ctx.cwd, prompt);
460
757
 
461
758
  // Send the assembled prompt to the main agent
462
759
  pi.sendUserMessage(prompt, { deliverAs: "followUp" });
463
-
464
- // Also show the prompt in a notification for reference
465
- ctx.ui.notify(`📝 /dev-${type} 提示词已投递,主代理正在处理`, "info");
466
760
  }
467
761
 
468
762
  // ── Questions for each command ────────────────────────────────
@@ -555,66 +849,55 @@ const COMPARE_QUESTIONS = [
555
849
  export default function (pi: ExtensionAPI) {
556
850
  // ── /dev-feat ──────────────────────────────────────────────
557
851
  pi.registerCommand("dev-feat", {
558
- description: "(prompt wizard) 新功能/创意生成 — 支持设计评审 (Grill) + PRD 生成",
852
+ description: "(prompt wizard) 新功能/创意生成 — 支持设计评审 (Grill) + 自动化工作流",
559
853
  handler: async (_args, ctx) => {
560
- // ── Phase 1: Wizard ──────────────────────────────────
561
- ctx.ui.notify("📋 /dev-feat — 新功能/创意生成,请逐项填写以下信息(留空跳过对应段落,Esc 取消)", "info");
562
-
563
854
  const answers: Record<string, string> = {};
564
- for (const q of FEAT_QUESTIONS) {
565
- const val = await ask(ctx, q.label, q.placeholder);
855
+ let featIdx = 0;
856
+ while (featIdx >= 0 && featIdx < FEAT_QUESTIONS.length) {
857
+ const q = FEAT_QUESTIONS[featIdx]!;
858
+ const existingVal = answers[q.key];
859
+ const placeholder = existingVal
860
+ ? `(之前: ${existingVal.slice(0, 60)}) ${q.placeholder}`
861
+ : q.placeholder;
862
+ const val = await ask(ctx, q.label, placeholder, true, existingVal || "");
566
863
  if (val === undefined) {
567
- ctx.ui.notify("❌ 已取消", "warning");
864
+ return;
865
+ }
866
+ if (val === BACK_MARKER) {
867
+ if (featIdx > 0) {
868
+ featIdx--;
869
+ continue;
870
+ }
568
871
  return;
569
872
  }
570
873
  answers[q.key] = val;
874
+ featIdx++;
571
875
  }
572
876
 
573
- // ── Phase 2: Assemble base prompt ───────────────────
574
877
  const basePrompt = assembleFeatPrompt(answers as FeatFields);
575
- ctx.ui.notify(`✅ 基本信息已收集,共 ${FEAT_QUESTIONS.length} 项`, "success");
576
878
 
577
- // ── Phase 3: Grill (设计评审) ───────────────────────
578
879
  const grillResult = await runGrillPhase(basePrompt, ctx);
579
880
  if (grillResult.cancelled) {
580
- ctx.ui.notify("❌ 操作已取消", "warning");
581
881
  return;
582
882
  }
583
883
  const finalPrompt = grillResult.enhancedPrompt;
584
884
 
585
- // ── Phase 4: Send to main agent ─────────────────────
885
+ if (FEAT_WORKFLOW_STEPS.length > 0) {
886
+ const handled = await promptWorkflowDecision(ctx, pi, finalPrompt, FEAT_WORKFLOW_STEPS);
887
+ if (handled) return;
888
+ }
889
+
586
890
  if (!finalPrompt) {
587
- ctx.ui.notify("⚠️ 最终提示词为空,正在尝试从备份文件恢复...", "warning");
588
891
  const recovered = recoverFromBackup(ctx.cwd);
589
892
  if (recovered) {
590
- ctx.ui.notify("✅ 已从备份文件恢复提示词内容", "success");
591
- const recoveredPath = saveAnswerFile(ctx.cwd, recovered);
592
- ctx.ui.notify(`✅ 提示词已组装完成,正在发送给主代理...`, "success");
893
+ const answerPath = saveAnswerFile(ctx.cwd, recovered);
593
894
  pi.sendUserMessage(recovered, { deliverAs: "followUp" });
594
- ctx.ui.notify(`📝 /dev-feat 提示词已投递(恢复自备份),主代理正在处理。备份: ${recoveredPath}`, "info");
595
- // ── Phase 5: Wait for agent to finish ───────────────
596
- await ctx.waitForIdle();
597
- // ── Phase 6: PRD generation ─────────────────────────
598
- const moduleHint = (answers as FeatFields).module || "feature";
599
- await runPRDPhase(recovered, moduleHint, pi, ctx);
600
- return;
601
- } else {
602
- ctx.ui.notify("❌ 错误:最终提示词为空且无可用备份,无法发送", "error");
603
895
  return;
604
896
  }
897
+ return;
605
898
  }
606
899
  const answerPath = saveAnswerFile(ctx.cwd, finalPrompt);
607
- ctx.ui.notify(`✅ 提示词已组装完成,正在发送给主代理...`, "success");
608
900
  pi.sendUserMessage(finalPrompt, { deliverAs: "followUp" });
609
- ctx.ui.notify(`📝 /dev-feat 提示词已投递,主代理正在处理。备份: ${answerPath}`, "info");
610
-
611
- // ── Phase 5: Wait for agent to finish ───────────────
612
- await ctx.waitForIdle();
613
-
614
- // ── Phase 6: PRD generation ─────────────────────────
615
- const moduleHint = (answers as FeatFields).module || "feature";
616
- const grillContext = finalPrompt;
617
- await runPRDPhase(grillContext, moduleHint, pi, ctx);
618
901
  },
619
902
  });
620
903
 
@@ -628,10 +911,11 @@ export default function (pi: ExtensionAPI) {
628
911
  {
629
912
  agentDef: _fixGrillAgent,
630
913
  title: "🐛 Bug 根因分析评审",
631
- description: "是否进入 Bug 根因分析评审 (Grill) 模式?\nAI 会从复现条件、根因推理、修复方案、回归风险等维度挑战你的理解。",
914
+ description: "AI 会从复现条件、根因推理、修复方案、回归风险等维度挑战你的理解。",
632
915
  questionTitle: "Bug 根因分析",
633
916
  loaderLabel: "🧠 AI 正在分析代码并生成根因评审问题...",
634
917
  },
918
+ { steps: FIX_WORKFLOW_STEPS },
635
919
  );
636
920
  },
637
921
  });
@@ -646,10 +930,11 @@ export default function (pi: ExtensionAPI) {
646
930
  {
647
931
  agentDef: _docGrillAgent,
648
932
  title: "📄 文档大纲评审",
649
- description: "是否进入文档大纲评审 (Grill) 模式?\nAI 会从受众定位、结构安排、示例选择等维度审视你的文档计划。",
933
+ description: "AI 会从受众定位、结构安排、示例选择等维度审视你的文档计划。",
650
934
  questionTitle: "文档大纲评审",
651
935
  loaderLabel: "🧠 AI 正在分析并生成文档大纲评审问题...",
652
936
  },
937
+ { steps: DOC_WORKFLOW_STEPS },
653
938
  );
654
939
  },
655
940
  });
@@ -664,10 +949,11 @@ export default function (pi: ExtensionAPI) {
664
949
  {
665
950
  agentDef: _refactorGrillAgent,
666
951
  title: "🔧 重构方案评审",
667
- description: "是否进入重构方案评审 (Grill) 模式?\nAI 会从模块边界、API 兼容性、测试策略、迁移风险等维度审视你的重构计划。",
952
+ description: "AI 会从模块边界、API 兼容性、测试策略、迁移风险等维度审视你的重构计划。",
668
953
  questionTitle: "重构方案评审",
669
954
  loaderLabel: "🧠 AI 正在分析代码并生成重构评审问题...",
670
955
  },
956
+ { steps: REFACTOR_WORKFLOW_STEPS },
671
957
  );
672
958
  },
673
959
  });
@@ -682,10 +968,11 @@ export default function (pi: ExtensionAPI) {
682
968
  {
683
969
  agentDef: _testGrillAgent,
684
970
  title: "🧪 测试计划评审",
685
- description: "是否进入测试计划评审 (Grill) 模式?\nAI 会从覆盖维度、边界条件、模拟策略等角度审视你的测试方案。",
971
+ description: "AI 会从覆盖维度、边界条件、模拟策略等角度审视你的测试方案。",
686
972
  questionTitle: "测试计划评审",
687
973
  loaderLabel: "🧠 AI 正在分析并生成测试评审问题...",
688
974
  },
975
+ { steps: TEST_WORKFLOW_STEPS },
689
976
  );
690
977
  },
691
978
  });
@@ -708,10 +995,11 @@ export default function (pi: ExtensionAPI) {
708
995
  {
709
996
  agentDef: _perfGrillAgent,
710
997
  title: "⚡ 性能优化方案评审",
711
- description: "是否进入性能优化方案评审 (Grill) 模式?\nAI 会从基准测试方法、优化方向、回归风险等维度审视你的方案。",
998
+ description: "AI 会从基准测试方法、优化方向、回归风险等维度审视你的方案。",
712
999
  questionTitle: "性能优化方案评审",
713
1000
  loaderLabel: "🧠 AI 正在分析并生成性能优化评审问题...",
714
1001
  },
1002
+ { steps: PERF_WORKFLOW_STEPS },
715
1003
  );
716
1004
  },
717
1005
  });
@@ -720,7 +1008,7 @@ export default function (pi: ExtensionAPI) {
720
1008
  pi.registerCommand("dev-style", {
721
1009
  description: "(prompt wizard) 风格/格式调整 — 交互填写后发送优化提示词给主代理",
722
1010
  handler: async (_args, ctx) => {
723
- await runWizard(ctx, pi, "style", "风格/格式调整", STYLE_QUESTIONS, assembleStylePrompt);
1011
+ await runWizard(ctx, pi, "style", "风格/格式调整", STYLE_QUESTIONS, assembleStylePrompt, { steps: STYLE_WORKFLOW_STEPS });
724
1012
  },
725
1013
  });
726
1014
 
@@ -728,7 +1016,7 @@ export default function (pi: ExtensionAPI) {
728
1016
  pi.registerCommand("dev-security", {
729
1017
  description: "(prompt wizard) 安全审查 — 交互填写后发送优化提示词给主代理",
730
1018
  handler: async (_args, ctx) => {
731
- await runWizard(ctx, pi, "security", "安全审查", SECURITY_QUESTIONS, assembleSecurityPrompt);
1019
+ await runWizard(ctx, pi, "security", "安全审查", SECURITY_QUESTIONS, assembleSecurityPrompt, { steps: SECURITY_WORKFLOW_STEPS });
732
1020
  },
733
1021
  });
734
1022
 
@@ -747,4 +1035,16 @@ export default function (pi: ExtensionAPI) {
747
1035
  await runWizard(ctx, pi, "compare", "对比评估", COMPARE_QUESTIONS, assembleComparePrompt);
748
1036
  },
749
1037
  });
1038
+
1039
+ // ── /dev-workflow-continue — 恢复中断的工作流 ─────────────
1040
+ pi.registerCommand("dev-workflow-continue", {
1041
+ description: "恢复上次中断的自动化工作流(从 checkpoint 继续)",
1042
+ handler: async (_args, ctx) => {
1043
+ const cp = loadCheckpointFromFile(ctx.cwd);
1044
+ if (!cp) {
1045
+ return;
1046
+ }
1047
+ await runWorkflow(ctx, pi, cp.prompt, { steps: FEAT_WORKFLOW_STEPS }, "恢复");
1048
+ },
1049
+ });
750
1050
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
15
  import { discoverAgents, spawnSubagent, extractFinalOutput, type AgentDef } from "./sub-agents";
16
+ import { uiInput } from "./ui-helpers";
16
17
 
17
18
  // ── Helpers ──────────────────────────────────────────────────
18
19
 
@@ -165,18 +166,13 @@ export default function (pi: ExtensionAPI) {
165
166
 
166
167
  let message = args.trim();
167
168
  if (!message) {
168
- const input = await ctx.ui.input("Commit message", {
169
- placeholder: "直接回车让 AI 自动生成,或输入信息后提交...",
170
- required: false,
171
- });
169
+ const input = await uiInput(ctx, "Commit message", "直接回车让 AI 自动生成,或输入信息后提交...");
172
170
  if (input === undefined) {
173
- ctx.ui.notify("Commit cancelled", "warning");
174
171
  return;
175
172
  }
176
173
  message = input.trim();
177
174
  }
178
175
 
179
- ctx.ui.notify("🤖 正在委派 git-sub-agent 处理...", "info");
180
176
 
181
177
  const task = message
182
178
  ? `Stage all changes with git add -A, then commit with message: "${message}". Do NOT ask for confirmation.`
@@ -193,7 +189,6 @@ export default function (pi: ExtensionAPI) {
193
189
  const agent = getAgent(ctx, gitAgent, "git-agent");
194
190
  if (!agent) return;
195
191
 
196
- ctx.ui.notify("🤖 正在委派 git-sub-agent 处理...", "info");
197
192
  await runSubAgent(
198
193
  agent,
199
194
  "Push commits to remote with git push. Do NOT ask for confirmation.",
@@ -211,18 +206,13 @@ export default function (pi: ExtensionAPI) {
211
206
 
212
207
  let message = args.trim();
213
208
  if (!message) {
214
- const input = await ctx.ui.input("Commit message", {
215
- placeholder: "直接回车让 AI 自动生成,或输入信息后提交并推送...",
216
- required: false,
217
- });
209
+ const input = await uiInput(ctx, "Commit message", "直接回车让 AI 自动生成,或输入信息后提交并推送...");
218
210
  if (input === undefined) {
219
- ctx.ui.notify("Commit & push cancelled", "warning");
220
211
  return;
221
212
  }
222
213
  message = input.trim();
223
214
  }
224
215
 
225
- ctx.ui.notify("🤖 正在委派 git-sub-agent 处理...", "info");
226
216
 
227
217
  const task = message
228
218
  ? `Stage all changes with git add -A, commit with message: "${message}", then push. Do NOT ask for confirmation.`