@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.
- package/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +14 -0
- package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +150 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260520-153000-fix-workflow-engine-bugs.json +108 -0
- package/README.md +171 -29
- package/agents/review-agent.md +5 -5
- package/agents/workflow/docWriter-agent.md +29 -0
- package/agents/workflow/planner-agent.md +80 -0
- package/agents/workflow/reviewer-agent.md +44 -0
- package/agents/workflow/trimmer-agent.md +34 -0
- package/agents/workflow/worker-agent.md +29 -0
- package/extensions/dev-prompts.ts +375 -75
- package/extensions/git-commands.ts +3 -13
- package/extensions/grill-me-agent.ts +138 -66
- package/extensions/sub-agents.ts +32 -11
- package/extensions/ui-helpers.ts +1029 -0
- package/extensions/workflow-engine.ts +1748 -0
- package/package.json +1 -1
- package/skills/review-html/SKILL.md +2 -2
- package/skills/to-prd/SKILL.md +1 -1
- package/tests/test-grill-json-fix.mjs +243 -0
- package/tests/test-output-directory-structure.mjs +177 -0
- package/tests/test-save-answer-file-workflow.mjs +187 -0
- package/tests/test-workflow-config.mjs +244 -0
- package/tests/test-workflow-engine-bugs.mjs +349 -0
- package/tests/test-workflow-engine.mjs +518 -0
|
@@ -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
|
|
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
|
-
*
|
|
367
|
-
*
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) +
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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.`
|