@ghyper9023/pi-dev-workflow 0.6.3 → 0.8.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 (40) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/.pre-commit-config.yaml +75 -0
  3. package/.version/RELEASE-v0.7.0.md +85 -0
  4. package/.version/RELEASE-v0.8.0.md +93 -0
  5. package/README.md +178 -294
  6. package/extensions/dev-prompts.ts +121 -1033
  7. package/extensions/git-commands.ts +62 -171
  8. package/extensions/grill-me-agent.ts +160 -164
  9. package/extensions/pre-check.ts +190 -0
  10. package/extensions/review-detect.ts +123 -0
  11. package/extensions/session-utils.ts +168 -0
  12. package/extensions/ui-helpers.ts +22 -780
  13. package/package.json +2 -2
  14. package/prompts/APPEND_SYSTEM.md +73 -59
  15. package/skills/review-html/SKILL.md +1 -1
  16. package/tests/test-no-subagents.mjs +264 -0
  17. package/.doc/AGENT-FRONTMATTER-REFERENCE.md +0 -198
  18. package/agents/git-agent.md +0 -44
  19. package/agents/grill/dev-doc-grill-agent.md +0 -42
  20. package/agents/grill/dev-fix-grill-agent.md +0 -44
  21. package/agents/grill/dev-grill-agent.md +0 -40
  22. package/agents/grill/dev-perf-grill-agent.md +0 -45
  23. package/agents/grill/dev-prd-agent.md +0 -60
  24. package/agents/grill/dev-refactor-grill-agent.md +0 -46
  25. package/agents/grill/dev-test-grill-agent.md +0 -45
  26. package/agents/review-agent.md +0 -53
  27. package/agents/workflow/docWriter-agent.md +0 -53
  28. package/agents/workflow/planner-agent.md +0 -131
  29. package/agents/workflow/reviewer-agent.md +0 -128
  30. package/agents/workflow/trimmer-agent.md +0 -78
  31. package/agents/workflow/worker-agent.md +0 -70
  32. package/extensions/sub-agents.ts +0 -954
  33. package/extensions/workflow-engine.ts +0 -2005
  34. package/tests/test-grill-json-fix.mjs +0 -243
  35. package/tests/test-loopcount-timeout-fix.mjs +0 -336
  36. package/tests/test-output-directory-structure.mjs +0 -177
  37. package/tests/test-save-answer-file-workflow.mjs +0 -187
  38. package/tests/test-workflow-config.mjs +0 -244
  39. package/tests/test-workflow-engine-bugs.mjs +0 -908
  40. package/tests/test-workflow-engine.mjs +0 -518
@@ -1,21 +1,26 @@
1
1
  /**
2
- * grill-me-agent.ts — 设计 (Grill) 和 PRD 生成的独立管理器
2
+ * grill-me-agent.ts — 设计追问(Grill)与 PRD 生成的运行时(运行在当前代理中)
3
3
  *
4
4
  * 职责:
5
- * 1. runGrillPhase() — 启动 sub-agent 生成评审问题,TUI 逐题呈现(选项 + 自定义输入)
6
- * 2. runPRDPhase() — 启动 sub-agent 生成 PRD,保存到 .pi-dev-output/pi-prd/
5
+ * 1. 注册 /grill 命令 — 对方案做追问式打磨,问答附到方案后交给当前代理
6
+ * 2. 注册 /prd 命令 — 按需求描述生成 PRD,保存到 .pi-dev-output/pi-prd/
7
+ * 3. runGrillPhase() — 由当前代理生成评审问题,TUI 逐题呈现(选项 + 自定义输入)
8
+ * 4. runPRDPhase() — 由当前代理生成 PRD,并询问是否立即开始开发
7
9
  *
8
- * 关键设计决策(修复 #2):
9
- * sub-agent 通过 `write` 工具将评审问题写入临时文件,主进程事后读取。
10
- * 不依赖从 NDJSON 响应文本中解析 JSON(多轮 tool-calling 场景不可靠)。
10
+ * 两个命令各自独立,不依附于 /dev-* 命令。
11
+ *
12
+ * 与旧版的区别:不再创建隔离的子代理进程,而是把追问/PRD 任务交给当前代理执行。
13
+ * 当前模型普遍具备 >=1M 上下文窗口,无需通过子代理隔离上下文。
14
+ *
15
+ * 关键设计:
16
+ * 当前代理通过 `write` 工具将结果写入临时文件,主进程事后读取。
17
+ * 不依赖从响应文本中解析 JSON(多轮 tool-calling 场景不可靠)。
11
18
  */
12
19
 
13
20
  import * as fs from "node:fs";
14
21
  import * as path from "node:path";
15
22
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
16
- import { BorderedLoader, DynamicBorder } from "@earendil-works/pi-coding-agent";
17
- import { spawnSubagent, extractFinalOutput, discoverAgents, type AgentDef } from "./sub-agents";
18
- export type { AgentDef };
23
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
19
24
  import {
20
25
  Container,
21
26
  SelectList,
@@ -27,11 +32,12 @@ import {
27
32
  wrapTextWithAnsi,
28
33
  type SelectItem,
29
34
  } from "@earendil-works/pi-tui";
30
- import { uiSelect, uiConfirm, uiInput } from "./ui-helpers";
35
+ import { uiSelect, uiConfirm, uiInput, uiTaskArg } from "./ui-helpers";
36
+ import { pollFor, getLastAssistantTextAfter } from "./session-utils";
31
37
 
32
38
  // ── Types ────────────────────────────────────────────────────
33
39
 
34
- /** A single grill question returned by the sub-agent. */
40
+ /** A single grill question returned by the agent. */
35
41
  export interface GrillQuestion {
36
42
  id: number;
37
43
  question: string;
@@ -101,26 +107,6 @@ export function saveAnswerFile(cwd: string, content: string): string {
101
107
  return path.join(DEV_OUTPUT_DIR, GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME, filename);
102
108
  }
103
109
 
104
- /**
105
- * Find the most recent answer backup file and read its content.
106
- * Returns undefined if no backup exists or read fails.
107
- * Now reads from pi-grill/answers/ subdirectory.
108
- */
109
- export function recoverFromBackup(cwd: string): string | undefined {
110
- const dir = path.join(cwd, DEV_OUTPUT_DIR, GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME);
111
- try {
112
- if (!fs.existsSync(dir)) return undefined;
113
- const files = fs.readdirSync(dir)
114
- .filter(f => f.startsWith("answer-") && f.endsWith(".md"))
115
- .map(f => ({ name: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
116
- .sort((a, b) => b.mtime - a.mtime);
117
- if (files.length === 0) return undefined;
118
- return fs.readFileSync(path.join(dir, files[0].name), "utf-8");
119
- } catch {
120
- return undefined;
121
- }
122
- }
123
-
124
110
  /** Generate a safe PRD filename. */
125
111
  function generatePrdFilename(moduleSuggestion: string): string {
126
112
  const safe = moduleSuggestion
@@ -132,10 +118,10 @@ function generatePrdFilename(moduleSuggestion: string): string {
132
118
  return `${safe || "feature"}-${ts}.md`;
133
119
  }
134
120
 
135
- // ── Sub-agent definitions ────────────────────────────────────
121
+ // ── File-based result extraction ─────────────────────────────
136
122
 
137
123
  /**
138
- * Helper: build the system prompt suffix that tells the sub-agent to
124
+ * Helper: build the system prompt suffix that tells the agent to
139
125
  * write results to a file via the `write` tool instead of putting JSON
140
126
  * in its chat response.
141
127
  */
@@ -176,15 +162,15 @@ function writeToolPromptSuffix(outputFilePath: string): string {
176
162
  '}',
177
163
  "```",
178
164
  "",
179
- "### \u26a0\ufe0f CRITICAL: String escaping rules",
165
+ "### ⚠️ CRITICAL: String escaping rules",
180
166
  "",
181
167
  "Every string value (question text, option text) MUST be valid JSON-escaped:",
182
168
  "- Double quotes inside text \u2192 \\\"",
183
- "- Newlines \u2192 \\n",
184
- "- Backslashes \u2192 \\\\",
185
- "- Tabs \u2192 \\t",
169
+ "- Newlines → \\n",
170
+ "- Backslashes → \\\\",
171
+ "- Tabs → \\t",
186
172
  "",
187
- "### \u2705 Self-review before writing",
173
+ "### ✅ Self-review before writing",
188
174
  "",
189
175
  "Before calling the `write` tool, mentally validate your JSON.",
190
176
  "Check that all strings are properly escaped and the structure matches the schema above.",
@@ -204,7 +190,7 @@ function writeToolPromptSuffix(outputFilePath: string): string {
204
190
  ' },',
205
191
  ' {',
206
192
  ' "id": 2,',
207
- ' "question": "\\u4ed6\\u8bf4\\u201c\\u8fd9\\u4e2a\\u4e0d\\u884c\\u201d\\uff0c\\u8be5\\u5982\\u4f55\\u5904\\u7406\\uff1f",',
193
+ ' "question": "\\u4ed6\\u8bf4\\u201c\\u8fd9\\u4e2a\\u4e88\\u884c\\u201d\\uff0c\\u8be5\\u5982\\u4f55\\u5904\\u7406\\uff1f",',
208
194
  ' "options": [',
209
195
  ' "\\u5ffd\\u7565",',
210
196
  ' "\\u4fee\\u590d"',
@@ -219,15 +205,8 @@ function writeToolPromptSuffix(outputFilePath: string): string {
219
205
  ].join("\n");
220
206
  }
221
207
 
222
- // ── Default agent definitions (loaded from agents/ directory) ────
223
-
224
- const _defaultGrillAgent = discoverAgents().find(a => a.name === "dev-grill-agent")!;
225
- const _defaultPrdAgent = discoverAgents().find(a => a.name === "dev-prd-agent")!;
226
-
227
- // ── File-based question extraction ───────────────────────────
228
-
229
208
  /**
230
- * Try to read and parse questions from the output file that the sub-agent
209
+ * Try to read and parse questions from the output file that the agent
231
210
  * was instructed to write via the `write` tool.
232
211
  */
233
212
  function readQuestionsFromFile(filePath: string): GrillQuestion[] {
@@ -258,7 +237,7 @@ function readQuestionsFromFile(filePath: string): GrillQuestion[] {
258
237
  }
259
238
 
260
239
  /**
261
- * Parse sub-agent output into a list of GrillQuestions.
240
+ * Parse agent output into a list of GrillQuestions.
262
241
  * Used as fallback when the file-based approach didn't produce results.
263
242
  */
264
243
  export function parseGrillQuestions(raw: string): GrillQuestion[] {
@@ -344,17 +323,15 @@ function extractQuestionArray(raw: string): Array<{ question: string; options: s
344
323
  * Options for customizing the grill phase.
345
324
  */
346
325
  export interface GrillOptions {
347
- agentDef?: AgentDef;
348
326
  title?: string;
349
327
  description?: string;
350
328
  questionTitle?: string;
351
- loaderLabel?: string;
352
329
  }
353
330
 
354
331
  /**
355
332
  * Run the grill phase:
356
333
  * 1. Confirm with user
357
- * 2. Call sub-agent → sub-agent writes questions to a file via `write` tool
334
+ * 2. Send task to current agent → agent writes questions to a file via `write` tool
358
335
  * 3. Read questions from the file
359
336
  * 4. Show each question as TUI SelectList + custom input option
360
337
  * 5. Collect all Q&A pairs
@@ -363,6 +340,7 @@ export interface GrillOptions {
363
340
  export async function runGrillPhase(
364
341
  assembledPrompt: string,
365
342
  ctx: ExtensionCommandContext,
343
+ pi: ExtensionAPI,
366
344
  options?: GrillOptions,
367
345
  ): Promise<GrillResult> {
368
346
  const defaultResult: GrillResult = {
@@ -371,11 +349,9 @@ export async function runGrillPhase(
371
349
  enhancedPrompt: assembledPrompt,
372
350
  };
373
351
 
374
- const agentDef = options?.agentDef ?? _defaultGrillAgent;
375
352
  const confirmTitle = options?.title ?? "🔍 设计方案追问完善";
376
353
  const confirmDesc = options?.description ?? "AI 会通过系统性追问帮你打磨方案:从术语精确化到边界条件验证,确保架构决策的每个分支都经过推敲。";
377
354
  const qTitlePrefix = options?.questionTitle ?? "设计方案追问完善";
378
- const loaderLabel = options?.loaderLabel ?? "🧠 AI 子代理正在分析代码并生成追问问题...";
379
355
 
380
356
  // ── Step 1: Confirm entering grill mode ──────────────────
381
357
  const enterGrill = await uiConfirm(ctx, confirmTitle, confirmDesc);
@@ -391,37 +367,17 @@ export async function runGrillPhase(
391
367
  writeToolPromptSuffix(outputFilePath),
392
368
  ].join("\n\n");
393
369
 
394
- // ── Step 3: Call sub-agent with BorderedLoader ───────────
395
- const questions = await ctx.ui.custom<GrillQuestion[]>((tui, theme, _kb, done) => {
396
- const loader = new BorderedLoader(tui, theme, loaderLabel);
397
- loader.onAbort = () => done([]);
398
-
399
- spawnSubagent(
400
- agentDef,
401
- enhancedPrompt,
402
- ctx.cwd,
403
- loader.signal,
404
- undefined,
405
- (progress) => {
406
- const inner = (loader as unknown as { loader?: { setText?: (t: string) => void } }).loader;
407
- inner?.setText?.(`🧠 ${progress.slice(0, 60)}`);
408
- },
409
- )
410
- .then((result) => {
411
- let qs = readQuestionsFromFile(outputFilePath);
412
- if (qs.length === 0) {
413
- const output = extractFinalOutput(result.output);
414
- qs = parseGrillQuestions(output);
415
- }
416
- done(qs);
417
- })
418
- .catch(() => {
419
- const qs = readQuestionsFromFile(outputFilePath);
420
- done(qs);
421
- });
370
+ // ── Step 3: Ask current agent to generate questions ──────
371
+ ctx.ui.notify("🧠 已提交给当前代理生成追问问题,请稍候...", "info");
372
+ const sentAt = Date.now();
373
+ pi.sendUserMessage(enhancedPrompt, { deliverAs: "followUp" });
422
374
 
423
- return loader;
424
- });
375
+ const questions = (await pollFor(() => {
376
+ const qs = readQuestionsFromFile(outputFilePath);
377
+ if (qs.length) return qs;
378
+ const parsed = parseGrillQuestions(getLastAssistantTextAfter(ctx, sentAt));
379
+ return parsed.length ? parsed : undefined;
380
+ }, 5 * 60_000)) || [];
425
381
 
426
382
  // ── Step 4: Retry dialog if no questions generated ──────
427
383
  if (questions.length === 0) {
@@ -449,43 +405,41 @@ export async function runGrillPhase(
449
405
  switch (choice) {
450
406
  case "🔄 重新尝试生成追问问题": {
451
407
  const retryPath = grillOutputPath(ctx.cwd);
452
- const errorFeedback = parseErrorMsg
453
- ? [
454
- "",
455
- "### ⚠️ Previous attempt had JSON errors — fix them now",
456
- "",
457
- `The previous attempt wrote to \`${outputFilePath}\` but the JSON was invalid.`,
458
- "",
459
- `JSON parse error: ${parseErrorMsg}`,
460
- "",
461
- "Invalid file content (first 2000 chars):",
462
- "```",
463
- failedFileContent.slice(0, 1000),
464
- "```",
465
- "",
466
- "Please write valid JSON to the new path below. Make sure all strings are properly JSON-escaped.",
467
- ].join("\n")
468
- : "";
469
408
  const retryPrompt = [
470
409
  assembledPrompt,
471
410
  writeToolPromptSuffix(retryPath),
472
- errorFeedback,
411
+ parseErrorMsg
412
+ ? [
413
+ "",
414
+ "### ⚠️ Previous attempt had JSON errors — fix them now",
415
+ "",
416
+ `The previous attempt wrote to \`${outputFilePath}\` but the JSON was invalid.`,
417
+ "",
418
+ `JSON parse error: ${parseErrorMsg}`,
419
+ "",
420
+ "Invalid file content (first 2000 chars):",
421
+ "```",
422
+ failedFileContent.slice(0, 1000),
423
+ "```",
424
+ "",
425
+ "Please write valid JSON to the new path below. Make sure all strings are properly JSON-escaped.",
426
+ ].join("\n")
427
+ : "",
473
428
  ].filter(Boolean).join("\n\n");
474
- const retryQuestions = await ctx.ui.custom<GrillQuestion[]>((tui, theme, _kb, done) => {
475
- const loader = new BorderedLoader(tui, theme, loaderLabel);
476
- loader.onAbort = () => done([]);
477
- spawnSubagent(agentDef, retryPrompt, ctx.cwd, loader.signal, undefined)
478
- .then((r) => {
479
- let qs = readQuestionsFromFile(retryPath);
480
- if (qs.length === 0) qs = parseGrillQuestions(extractFinalOutput(r.output));
481
- done(qs);
482
- })
483
- .catch(() => done([]));
484
- return loader;
485
- });
429
+
430
+ const retrySentAt = Date.now();
431
+ pi.sendUserMessage(retryPrompt, { deliverAs: "followUp" });
432
+
433
+ const retryQuestions = (await pollFor(() => {
434
+ const qs = readQuestionsFromFile(retryPath);
435
+ if (qs.length) return qs;
436
+ const parsed = parseGrillQuestions(getLastAssistantTextAfter(ctx, retrySentAt));
437
+ return parsed.length ? parsed : undefined;
438
+ }, 5 * 60_000)) || [];
486
439
  if (retryQuestions.length === 0) {
487
440
  return defaultResult;
488
441
  }
442
+
489
443
  // Replace questions with retry results (with back support)
490
444
  const pairs: Array<{ question: string; answer: string }> = [];
491
445
  let rIdx = 0;
@@ -504,7 +458,6 @@ export async function runGrillPhase(
504
458
  }
505
459
  return { ...defaultResult, cancelled: true, pairs };
506
460
  }
507
- // Overwrite if re-answering, otherwise append
508
461
  if (rIdx < pairs.length) {
509
462
  pairs[rIdx] = { question: q.question, answer };
510
463
  } else {
@@ -561,7 +514,6 @@ export async function runGrillPhase(
561
514
  return { ...defaultResult, cancelled: true, pairs };
562
515
  }
563
516
 
564
- // Overwrite if re-answering (back then forward), otherwise append
565
517
  if (qIdx < pairs.length) {
566
518
  pairs[qIdx] = { question: q.question, answer };
567
519
  } else {
@@ -600,15 +552,6 @@ export async function runGrillPhase(
600
552
  * - ↑↓ 选择, Enter 确认, Esc 取消全部评审
601
553
  * - Ctrl+Shift+← 返回上一题(仅当 backable=true 且 currentIndex > 1 时生效)
602
554
  * - 选择 "✏️ 自定义输入" 进入文本输入模式
603
- *
604
- * @param ctx - Extension command context for TUI rendering
605
- * @param q - The grill question to display
606
- * @param currentIndex - 1-based index of current question
607
- * @param totalCount - Total number of questions
608
- * @param titlePrefix - Prefix for the question title bar
609
- * @param backable - Whether navigating back to previous question is allowed
610
- * @param previousAnswer - Previous answer to pre-fill as "上次选择" marker
611
- * @returns Selected option text, "__BACK__" for back navigation, or null for cancel
612
555
  */
613
556
  async function showQuestionTUI(
614
557
  ctx: ExtensionCommandContext,
@@ -631,7 +574,6 @@ async function showQuestionTUI(
631
574
  return {
632
575
  value: `opt-${i}`,
633
576
  label: truncated,
634
- // 完整文本由下方的预览面板展示(支持换行),description 列无法换行故移除
635
577
  };
636
578
  });
637
579
 
@@ -683,7 +625,6 @@ async function showQuestionTUI(
683
625
  container.addChild(new Spacer(1));
684
626
  container.addChild(previewText);
685
627
 
686
- // 初始化预览为第一个选项
687
628
  if (q.options.length > 0) {
688
629
  const initialWrapped = wrapTextWithAnsi(q.options[0], previewWidth);
689
630
  previewText.setText(
@@ -718,7 +659,6 @@ async function showQuestionTUI(
718
659
  render: (w) => container.render(w),
719
660
  invalidate: () => container.invalidate(),
720
661
  handleInput: (data) => {
721
- // Ctrl+Shift+← → 返回上一题(SelectList 不处理该键,需自行拦截)
722
662
  if (backable && currentIndex > 1 && matchesKey(data, Key.ctrlShift("left"))) {
723
663
  done("__BACK__");
724
664
  return;
@@ -753,7 +693,7 @@ async function showQuestionTUI(
753
693
  /**
754
694
  * Run the PRD phase:
755
695
  * 1. Ask user if they want to create a PRD
756
- * 2. Call sub-agent → gets PRD Markdown
696
+ * 2. Call current agent → gets PRD Markdown
757
697
  * 3. Save to .pi-dev-output/pi-prd/<name>.md
758
698
  * 4. Ask if user wants to start development
759
699
  */
@@ -770,37 +710,44 @@ export async function runPRDPhase(
770
710
  );
771
711
  if (!wantPrd) return null;
772
712
 
773
- const prdContent = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
774
- const loader = new BorderedLoader(tui, theme, "📝 AI 正在生成 PRD 文档...");
775
- loader.onAbort = () => done(null);
776
-
777
- const prdTask = [
778
- "请根据以下上下文生成一份 PRD(产品需求文档)。",
779
- "输出格式必须是完整的 Markdown 文档。",
780
- "",
781
- "=== 上下文 ===",
782
- context,
783
- ].join("\n");
784
-
785
- spawnSubagent(_defaultPrdAgent, prdTask, ctx.cwd, loader.signal)
786
- .then((result) => {
787
- const output = extractFinalOutput(result.output);
788
- done(output && output.length >= 50 ? output : null);
789
- })
790
- .catch(() => done(null));
791
-
792
- return loader;
793
- });
794
-
795
- if (!prdContent) {
796
- return null;
797
- }
798
-
799
713
  const prdDir = ensureOutputDir(ctx.cwd, PRD_DIRNAME);
800
714
  const filename = generatePrdFilename(moduleHint);
801
715
  const filePath = path.join(DEV_OUTPUT_DIR, PRD_DIRNAME, filename);
802
716
  const fullPath = path.join(prdDir, filename);
803
- fs.writeFileSync(fullPath, prdContent, "utf-8");
717
+
718
+ const prdTask = [
719
+ "请根据以下上下文生成一份 PRD(产品需求文档)。",
720
+ "输出格式必须是完整的 Markdown 文档。",
721
+ "",
722
+ "=== 上下文 ===",
723
+ context,
724
+ "",
725
+ `请使用 write 工具将完整 PRD 写入以下路径: ${fullPath}`,
726
+ "不要在聊天中输出全文,只需确认写入完成。",
727
+ ].join("\n");
728
+
729
+ ctx.ui.notify("📝 已提交给当前代理生成 PRD,请稍候...", "info");
730
+ const sentAt = Date.now();
731
+ pi.sendUserMessage(prdTask, { deliverAs: "followUp" });
732
+
733
+ const prdContent = (await pollFor(() => {
734
+ let c = "";
735
+ try {
736
+ c = fs.readFileSync(fullPath, "utf-8").trim();
737
+ } catch { /* file not written yet */ }
738
+ if (c) return c;
739
+ return getLastAssistantTextAfter(ctx, sentAt) || undefined;
740
+ }, 5 * 60_000)) || "";
741
+
742
+ if (!prdContent || prdContent.length < 50) {
743
+ ctx.ui.notify("❌ PRD 生成失败:未获取到有效 PRD 内容,请查看当前代理的回复后重试。", "error");
744
+ return null;
745
+ }
746
+
747
+ // Fallback: if the agent chatted the PRD instead of writing it, persist it ourselves.
748
+ if (!fs.existsSync(fullPath)) {
749
+ fs.writeFileSync(fullPath, prdContent, "utf-8");
750
+ }
804
751
 
805
752
  await askDevelopmentStart(pi, ctx, prdContent, filePath);
806
753
  return { content: prdContent, filePath };
@@ -868,13 +815,62 @@ async function askDevelopmentStart(
868
815
  }
869
816
  }
870
817
 
871
- // ── Extension factory (required by pi extension loader) ─────
872
- //
873
- // This file lives in extensions/ so pi will attempt to load it.
874
- // The default export satisfies the loader; the real functionality
875
- // is consumed by dev-prompts.ts via named imports.
876
- //
877
- export default function (_pi: ExtensionAPI) {
878
- // grill-me-agent is a helper module, not a standalone extension.
879
- // It is imported by dev-prompts.ts to provide grill + PRD phases.
818
+ // ── Extension factory ────────────────────────────────────
819
+
820
+ export default function (pi: ExtensionAPI) {
821
+ pi.registerCommand("grill", {
822
+ description: "(grill) 提交前追问式打磨方案 — AI 逐题追问,问答附到方案后交给当前代理执行",
823
+ handler: async (args, ctx) => {
824
+ if (!ctx.hasUI) {
825
+ ctx.ui.notify("/grill 需要交互式界面", "error");
826
+ return;
827
+ }
828
+
829
+ const plan = await uiTaskArg(
830
+ ctx,
831
+ args,
832
+ "待打磨的方案 / 任务描述(必填)",
833
+ "如 实现邮箱密码登录:注册、登录、会话保持",
834
+ );
835
+ if (!plan) return;
836
+
837
+ const result = await runGrillPhase(plan, ctx, pi, {
838
+ title: "方案追问完善",
839
+ description: "AI 会针对方案逐题追问:术语精确化、边界条件、失败路径、验证方式。",
840
+ questionTitle: "方案追问完善",
841
+ });
842
+ if (result.cancelled) {
843
+ ctx.ui.notify("已取消追问,未发送任何内容", "info");
844
+ return;
845
+ }
846
+ if (result.pairs.length === 0) {
847
+ ctx.ui.notify("未进入追问,未发送任何内容", "info");
848
+ return;
849
+ }
850
+
851
+ saveAnswerFile(ctx.cwd, result.enhancedPrompt);
852
+ pi.sendUserMessage(result.enhancedPrompt, { deliverAs: "followUp" });
853
+ ctx.ui.notify("追问完成,方案已发送给当前代理", "info");
854
+ },
855
+ });
856
+
857
+ pi.registerCommand("prd", {
858
+ description: "(prd) 按需求描述生成 PRD 文档,并可选直接开始开发",
859
+ handler: async (args, ctx) => {
860
+ if (!ctx.hasUI) {
861
+ ctx.ui.notify("/prd 需要交互式界面", "error");
862
+ return;
863
+ }
864
+
865
+ const requirement = await uiTaskArg(
866
+ ctx,
867
+ args,
868
+ "PRD 需求描述(必填)",
869
+ "如 支持邮箱密码注册登录,含密码重置",
870
+ );
871
+ if (!requirement) return;
872
+
873
+ await runPRDPhase(requirement, requirement.split("\n")[0] || "feature", pi, ctx);
874
+ },
875
+ });
880
876
  }