@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.
- package/.github/workflows/release.yml +56 -0
- package/.pre-commit-config.yaml +75 -0
- package/.version/RELEASE-v0.7.0.md +85 -0
- package/.version/RELEASE-v0.8.0.md +93 -0
- package/README.md +178 -294
- package/extensions/dev-prompts.ts +121 -1033
- package/extensions/git-commands.ts +62 -171
- package/extensions/grill-me-agent.ts +160 -164
- package/extensions/pre-check.ts +190 -0
- package/extensions/review-detect.ts +123 -0
- package/extensions/session-utils.ts +168 -0
- package/extensions/ui-helpers.ts +22 -780
- package/package.json +2 -2
- package/prompts/APPEND_SYSTEM.md +73 -59
- package/skills/review-html/SKILL.md +1 -1
- package/tests/test-no-subagents.mjs +264 -0
- package/.doc/AGENT-FRONTMATTER-REFERENCE.md +0 -198
- package/agents/git-agent.md +0 -44
- package/agents/grill/dev-doc-grill-agent.md +0 -42
- package/agents/grill/dev-fix-grill-agent.md +0 -44
- package/agents/grill/dev-grill-agent.md +0 -40
- package/agents/grill/dev-perf-grill-agent.md +0 -45
- package/agents/grill/dev-prd-agent.md +0 -60
- package/agents/grill/dev-refactor-grill-agent.md +0 -46
- package/agents/grill/dev-test-grill-agent.md +0 -45
- package/agents/review-agent.md +0 -53
- package/agents/workflow/docWriter-agent.md +0 -53
- package/agents/workflow/planner-agent.md +0 -131
- package/agents/workflow/reviewer-agent.md +0 -128
- package/agents/workflow/trimmer-agent.md +0 -78
- package/agents/workflow/worker-agent.md +0 -70
- package/extensions/sub-agents.ts +0 -954
- package/extensions/workflow-engine.ts +0 -2005
- package/tests/test-grill-json-fix.mjs +0 -243
- package/tests/test-loopcount-timeout-fix.mjs +0 -336
- package/tests/test-output-directory-structure.mjs +0 -177
- package/tests/test-save-answer-file-workflow.mjs +0 -187
- package/tests/test-workflow-config.mjs +0 -244
- package/tests/test-workflow-engine-bugs.mjs +0 -908
- package/tests/test-workflow-engine.mjs +0 -518
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* grill-me-agent.ts —
|
|
2
|
+
* grill-me-agent.ts — 设计追问(Grill)与 PRD 生成的运行时(运行在当前代理中)
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
5
|
+
* 1. 注册 /grill 命令 — 对方案做追问式打磨,问答附到方案后交给当前代理
|
|
6
|
+
* 2. 注册 /prd 命令 — 按需求描述生成 PRD,保存到 .pi-dev-output/pi-prd/
|
|
7
|
+
* 3. runGrillPhase() — 由当前代理生成评审问题,TUI 逐题呈现(选项 + 自定义输入)
|
|
8
|
+
* 4. runPRDPhase() — 由当前代理生成 PRD,并询问是否立即开始开发
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
// ──
|
|
121
|
+
// ── File-based result extraction ─────────────────────────────
|
|
136
122
|
|
|
137
123
|
/**
|
|
138
|
-
* Helper: build the system prompt suffix that tells the
|
|
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
|
-
"###
|
|
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
|
|
184
|
-
"- Backslashes
|
|
185
|
-
"- Tabs
|
|
169
|
+
"- Newlines → \\n",
|
|
170
|
+
"- Backslashes → \\\\",
|
|
171
|
+
"- Tabs → \\t",
|
|
186
172
|
"",
|
|
187
|
-
"###
|
|
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\\
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
}
|