@ghyper9023/pi-dev-workflow 0.3.1 → 0.4.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/README.md +171 -29
- package/agents/grill/dev-doc-grill-agent.md +34 -0
- package/agents/grill/dev-fix-grill-agent.md +35 -0
- package/agents/grill/dev-grill-agent.md +33 -0
- package/agents/grill/dev-perf-grill-agent.md +36 -0
- package/agents/grill/dev-prd-agent.md +53 -0
- package/agents/grill/dev-refactor-grill-agent.md +36 -0
- package/agents/grill/dev-test-grill-agent.md +35 -0
- 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 +408 -222
- package/extensions/git-commands.ts +3 -13
- package/extensions/grill-me-agent.ts +277 -150
- package/extensions/sub-agents.ts +53 -23
- package/extensions/ui-helpers.ts +1030 -0
- package/extensions/workflow-engine.ts +1715 -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.mjs +518 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
5
|
* 1. runGrillPhase() — 启动 sub-agent 生成评审问题,TUI 逐题呈现(选项 + 自定义输入)
|
|
6
|
-
* 2. runPRDPhase() — 启动 sub-agent 生成 PRD,保存到 pi-dev-output/pi-prd/
|
|
6
|
+
* 2. runPRDPhase() — 启动 sub-agent 生成 PRD,保存到 .pi-dev-output/pi-prd/
|
|
7
7
|
*
|
|
8
8
|
* 关键设计决策(修复 #2):
|
|
9
9
|
* sub-agent 通过 `write` 工具将评审问题写入临时文件,主进程事后读取。
|
|
@@ -14,7 +14,7 @@ import * as fs from "node:fs";
|
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { BorderedLoader, DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { spawnSubagent, extractFinalOutput, type AgentDef } from "./sub-agents";
|
|
17
|
+
import { spawnSubagent, extractFinalOutput, discoverAgents, type AgentDef } from "./sub-agents";
|
|
18
18
|
export type { AgentDef };
|
|
19
19
|
import {
|
|
20
20
|
Container,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Spacer,
|
|
24
24
|
type SelectItem,
|
|
25
25
|
} from "@earendil-works/pi-tui";
|
|
26
|
+
import { uiSelect, uiConfirm, uiInput } from "./ui-helpers";
|
|
26
27
|
|
|
27
28
|
// ── Types ────────────────────────────────────────────────────
|
|
28
29
|
|
|
@@ -53,31 +54,67 @@ export interface PRDResult {
|
|
|
53
54
|
|
|
54
55
|
// ── Output dirs ──────────────────────────────────────────────
|
|
55
56
|
|
|
56
|
-
const DEV_OUTPUT_DIR = "pi-dev-output";
|
|
57
|
+
const DEV_OUTPUT_DIR = ".pi-dev-output";
|
|
57
58
|
const GRILL_DIRNAME = "pi-grill";
|
|
59
|
+
const GRILL_ANSWERS_DIRNAME = "answers";
|
|
60
|
+
const GRILL_QUESTIONS_DIRNAME = "questions";
|
|
58
61
|
const PRD_DIRNAME = "pi-prd";
|
|
59
62
|
|
|
60
|
-
/** Ensure an output subdirectory exists
|
|
63
|
+
/** Ensure an output subdirectory exists. */
|
|
61
64
|
function ensureOutputDir(cwd: string, subdir: string): string {
|
|
62
65
|
const dir = path.join(cwd, DEV_OUTPUT_DIR, subdir);
|
|
63
66
|
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
-
const gitignorePath = path.join(cwd, DEV_OUTPUT_DIR, ".gitignore");
|
|
65
|
-
try {
|
|
66
|
-
const existing = fs.readFileSync(gitignorePath, "utf-8").trim();
|
|
67
|
-
if (!existing.includes("*")) {
|
|
68
|
-
fs.writeFileSync(gitignorePath, "*\n!.gitignore\n");
|
|
69
|
-
}
|
|
70
|
-
} catch {
|
|
71
|
-
fs.writeFileSync(gitignorePath, "*\n!.gitignore\n");
|
|
72
|
-
}
|
|
73
67
|
return dir;
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
/**
|
|
70
|
+
/** Format current time as YYYYMMDD-HHmm for human-readable timestamps. */
|
|
71
|
+
function formatTimestamp(): string {
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const Y = now.getFullYear().toString();
|
|
74
|
+
const M = (now.getMonth() + 1).toString().padStart(2, "0");
|
|
75
|
+
const D = now.getDate().toString().padStart(2, "0");
|
|
76
|
+
const h = now.getHours().toString().padStart(2, "0");
|
|
77
|
+
const m = now.getMinutes().toString().padStart(2, "0");
|
|
78
|
+
return `${Y}${M}${D}-${h}${m}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Generate a safe temp filename for grill output (pi-grill/questions/questions-<id>-<YYYYMMDD-HHmm>.json). */
|
|
77
82
|
function grillOutputPath(cwd: string): string {
|
|
78
|
-
const dir = ensureOutputDir(cwd, GRILL_DIRNAME);
|
|
83
|
+
const dir = ensureOutputDir(cwd, path.join(GRILL_DIRNAME, GRILL_QUESTIONS_DIRNAME));
|
|
79
84
|
const ts = Date.now().toString(36);
|
|
80
|
-
return path.join(dir, `questions-${ts}.json`);
|
|
85
|
+
return path.join(dir, `questions-${ts}-${formatTimestamp()}.json`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Save the final assembled prompt to a timestamped answer file (pi-grill/answers/answer-<id>-<YYYYMMDD-HHmm>.md).
|
|
90
|
+
* Returns the relative path from cwd (for display in notifications).
|
|
91
|
+
*/
|
|
92
|
+
export function saveAnswerFile(cwd: string, content: string): string {
|
|
93
|
+
const dir = ensureOutputDir(cwd, path.join(GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME));
|
|
94
|
+
const ts = Date.now().toString(36);
|
|
95
|
+
const filename = `answer-${ts}-${formatTimestamp()}.md`;
|
|
96
|
+
fs.writeFileSync(path.join(dir, filename), content, "utf-8");
|
|
97
|
+
return path.join(DEV_OUTPUT_DIR, GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME, filename);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Find the most recent answer backup file and read its content.
|
|
102
|
+
* Returns undefined if no backup exists or read fails.
|
|
103
|
+
* Now reads from pi-grill/answers/ subdirectory.
|
|
104
|
+
*/
|
|
105
|
+
export function recoverFromBackup(cwd: string): string | undefined {
|
|
106
|
+
const dir = path.join(cwd, DEV_OUTPUT_DIR, GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME);
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(dir)) return undefined;
|
|
109
|
+
const files = fs.readdirSync(dir)
|
|
110
|
+
.filter(f => f.startsWith("answer-") && f.endsWith(".md"))
|
|
111
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
112
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
113
|
+
if (files.length === 0) return undefined;
|
|
114
|
+
return fs.readFileSync(path.join(dir, files[0].name), "utf-8");
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
/** Generate a safe PRD filename. */
|
|
@@ -107,96 +144,81 @@ function writeToolPromptSuffix(outputFilePath: string): string {
|
|
|
107
144
|
"Instead, use the `write` tool to save the questions to a file.",
|
|
108
145
|
`Write to this exact path: ${outputFilePath}`,
|
|
109
146
|
"",
|
|
110
|
-
"The file content must be a valid JSON object
|
|
111
|
-
|
|
147
|
+
"The file content must be a **valid** JSON object conforming to this JSON Schema (Draft-07):",
|
|
148
|
+
"",
|
|
149
|
+
"```json",
|
|
150
|
+
'{',
|
|
151
|
+
' "$schema": "http://json-schema.org/draft-07/schema#",',
|
|
152
|
+
' "type": "object",',
|
|
153
|
+
' "required": ["questions"],',
|
|
154
|
+
' "properties": {',
|
|
155
|
+
' "questions": {',
|
|
156
|
+
' "type": "array",',
|
|
157
|
+
' "items": {',
|
|
158
|
+
' "type": "object",',
|
|
159
|
+
' "required": ["id", "question", "options"],',
|
|
160
|
+
' "properties": {',
|
|
161
|
+
' "id": { "type": "integer" },',
|
|
162
|
+
' "question": { "type": "string" },',
|
|
163
|
+
' "options": {',
|
|
164
|
+
' "type": "array",',
|
|
165
|
+
' "items": { "type": "string" },',
|
|
166
|
+
' "minItems": 1',
|
|
167
|
+
' }',
|
|
168
|
+
' }',
|
|
169
|
+
' }',
|
|
170
|
+
' }',
|
|
171
|
+
' }',
|
|
172
|
+
'}',
|
|
173
|
+
"```",
|
|
174
|
+
"",
|
|
175
|
+
"### \u26a0\ufe0f CRITICAL: String escaping rules",
|
|
176
|
+
"",
|
|
177
|
+
"Every string value (question text, option text) MUST be valid JSON-escaped:",
|
|
178
|
+
"- Double quotes inside text \u2192 \\\"",
|
|
179
|
+
"- Newlines \u2192 \\n",
|
|
180
|
+
"- Backslashes \u2192 \\\\",
|
|
181
|
+
"- Tabs \u2192 \\t",
|
|
182
|
+
"",
|
|
183
|
+
"### \u2705 Self-review before writing",
|
|
184
|
+
"",
|
|
185
|
+
"Before calling the `write` tool, mentally validate your JSON.",
|
|
186
|
+
"Check that all strings are properly escaped and the structure matches the schema above.",
|
|
187
|
+
"If you are unsure, write a quick test with `bash` (e.g. `node -e \"JSON.parse(...)\"`).",
|
|
188
|
+
"",
|
|
189
|
+
"Example output:",
|
|
190
|
+
"```json",
|
|
112
191
|
'{',
|
|
113
192
|
' "questions": [',
|
|
114
193
|
' {',
|
|
115
194
|
' "id": 1,',
|
|
116
|
-
' "question": "
|
|
117
|
-
' "options": [
|
|
195
|
+
' "question": "\\u9879\\u76ee\\u662f\\u5426\\u5b58\\u5728\\u6a21\\u5757\\u7ed3\\u6784\\uff1f",',
|
|
196
|
+
' "options": [',
|
|
197
|
+
' "\\u5df2\\u5b58\\u5728\\uff0c\\u4f8b\\u5982 src/controller/example.rs",',
|
|
198
|
+
' "\\u4ece\\u96f6\\u521b\\u5efa"',
|
|
199
|
+
' ]',
|
|
200
|
+
' },',
|
|
201
|
+
' {',
|
|
202
|
+
' "id": 2,',
|
|
203
|
+
' "question": "\\u4ed6\\u8bf4\\u201c\\u8fd9\\u4e2a\\u4e0d\\u884c\\u201d\\uff0c\\u8be5\\u5982\\u4f55\\u5904\\u7406\\uff1f",',
|
|
204
|
+
' "options": [',
|
|
205
|
+
' "\\u5ffd\\u7565",',
|
|
206
|
+
' "\\u4fee\\u590d"',
|
|
207
|
+
' ]',
|
|
118
208
|
' }',
|
|
119
209
|
' ]',
|
|
120
210
|
'}',
|
|
121
|
-
|
|
211
|
+
"```",
|
|
122
212
|
"",
|
|
123
213
|
"After writing, you may include a brief summary in your chat response.",
|
|
124
214
|
"But the JSON MUST be in the file, NOT in the chat.",
|
|
125
215
|
].join("\n");
|
|
126
216
|
}
|
|
127
217
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
systemPrompt: [
|
|
133
|
-
"You are an expert design reviewer. Interview the developer relentlessly about every aspect of the feature plan until reaching shared understanding.",
|
|
134
|
-
"",
|
|
135
|
-
"## Rules",
|
|
136
|
-
"- For EACH question, provide recommended answer OPTIONS (a/b/c format) that the user can pick from",
|
|
137
|
-
"- Walk down each branch of the design tree, resolving dependencies between decisions one-by-one",
|
|
138
|
-
"- Be specific — refer to actual code modules, file paths, and architecture decisions",
|
|
139
|
-
"- Explore the codebase (use read/bash tools) when a question can be answered by looking at existing code",
|
|
140
|
-
"- Ask questions about: architecture, data flow, edge cases, security, testing, module boundaries, dependencies, error handling, performance, scalability",
|
|
141
|
-
"- If terminology conflicts with existing project glossary (CONTEXT.md), call it out",
|
|
142
|
-
"- Sharpen fuzzy language — propose precise canonical terms",
|
|
143
|
-
"- Stress-test scenarios with specific edge cases",
|
|
144
|
-
"- Cross-reference with existing code — surface contradictions",
|
|
145
|
-
"",
|
|
146
|
-
"## Quantity",
|
|
147
|
-
"Ask as many questions as needed to thoroughly review the design.",
|
|
148
|
-
"Do not artificially limit the number — cover architecture, data flow, edge cases, security, testing, module boundaries, dependencies, error handling, and more.",
|
|
149
|
-
"Typically 15-40 questions for a moderate feature.",
|
|
150
|
-
"",
|
|
151
|
-
"## Language",
|
|
152
|
-
"Questions and options should be in the same language as the feature request (default: Chinese).",
|
|
153
|
-
].join("\n"),
|
|
154
|
-
timeoutMs: 300_000, // 5 min
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const PRD_AGENT_DEF: AgentDef = {
|
|
158
|
-
name: "prd-agent",
|
|
159
|
-
description: "PRD writer — synthesizes a PRD from conversation context",
|
|
160
|
-
tools: ["read", "bash"],
|
|
161
|
-
systemPrompt: [
|
|
162
|
-
"You are an expert product spec writer.",
|
|
163
|
-
"Your task is to create a PRD from the provided conversation context.",
|
|
164
|
-
"",
|
|
165
|
-
"## Rules",
|
|
166
|
-
"- Do NOT ask any questions — just synthesize what you already know",
|
|
167
|
-
"- Explore the repo to understand the current state of the codebase",
|
|
168
|
-
"- Use the project's domain vocabulary throughout",
|
|
169
|
-
"- Use the template below and output ONLY the Markdown content (no JSON wrapper, no preamble)",
|
|
170
|
-
"",
|
|
171
|
-
"## Template",
|
|
172
|
-
"",
|
|
173
|
-
"# {Feature Name} — PRD",
|
|
174
|
-
"",
|
|
175
|
-
"## Problem Statement",
|
|
176
|
-
"The problem that the user is facing, from the user's perspective.",
|
|
177
|
-
"",
|
|
178
|
-
"## Solution",
|
|
179
|
-
"The solution to the problem, from the user's perspective.",
|
|
180
|
-
"",
|
|
181
|
-
"## User Stories",
|
|
182
|
-
"A numbered list of user stories:",
|
|
183
|
-
"1. As an <actor>, I want a <feature>, so that <benefit>",
|
|
184
|
-
"",
|
|
185
|
-
"## Implementation Decisions",
|
|
186
|
-
"A list of implementation decisions including modules to build/modify, architectural decisions, schema changes, API contracts.",
|
|
187
|
-
"Do NOT include specific file paths or code snippets (may become outdated).",
|
|
188
|
-
"",
|
|
189
|
-
"## Testing Decisions",
|
|
190
|
-
"A description of what makes a good test, which modules will be tested, prior art.",
|
|
191
|
-
"",
|
|
192
|
-
"## Out of Scope",
|
|
193
|
-
"Things explicitly out of scope.",
|
|
194
|
-
"",
|
|
195
|
-
"## Further Notes",
|
|
196
|
-
"Any further notes about the feature.",
|
|
197
|
-
].join("\n"),
|
|
198
|
-
timeoutMs: 300_000,
|
|
199
|
-
};
|
|
218
|
+
// ── Default agent definitions (loaded from agents/ directory) ────
|
|
219
|
+
|
|
220
|
+
const _defaultGrillAgent = discoverAgents().find(a => a.name === "dev-grill-agent")!;
|
|
221
|
+
const _defaultPrdAgent = discoverAgents().find(a => a.name === "dev-prd-agent")!;
|
|
200
222
|
|
|
201
223
|
// ── File-based question extraction ───────────────────────────
|
|
202
224
|
|
|
@@ -220,7 +242,13 @@ function readQuestionsFromFile(filePath: string): GrillQuestion[] {
|
|
|
220
242
|
question: q.question,
|
|
221
243
|
options: q.options,
|
|
222
244
|
}));
|
|
223
|
-
} catch {
|
|
245
|
+
} catch (e) {
|
|
246
|
+
// Log parse errors for debugging (development feedback)
|
|
247
|
+
try {
|
|
248
|
+
const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8").slice(0, 1000) : "(file not found)";
|
|
249
|
+
console.error(`[grill-me-agent] JSON parse error in ${filePath}:`, (e as Error).message);
|
|
250
|
+
console.error(`[grill-me-agent] First 1000 chars:`, content);
|
|
251
|
+
} catch { /* ignore secondary errors */ }
|
|
224
252
|
return [];
|
|
225
253
|
}
|
|
226
254
|
}
|
|
@@ -339,16 +367,17 @@ export async function runGrillPhase(
|
|
|
339
367
|
enhancedPrompt: assembledPrompt,
|
|
340
368
|
};
|
|
341
369
|
|
|
342
|
-
const agentDef = options?.agentDef ??
|
|
370
|
+
const agentDef = options?.agentDef ?? _defaultGrillAgent;
|
|
343
371
|
const confirmTitle = options?.title ?? "🔍 设计方案评审";
|
|
344
|
-
const confirmDesc = options?.description ?? "
|
|
372
|
+
const confirmDesc = options?.description ?? "AI 会从架构、数据流、边界条件、安全等多个维度挑战你的设计。";
|
|
345
373
|
const qTitlePrefix = options?.questionTitle ?? "设计方案评审";
|
|
346
|
-
const loaderLabel = options?.loaderLabel ?? "🧠 AI
|
|
374
|
+
const loaderLabel = options?.loaderLabel ?? "🧠 AI 子代理正在分析代码并生成评审问题...";
|
|
347
375
|
|
|
348
376
|
// ── Step 1: Confirm entering grill mode ──────────────────
|
|
349
|
-
const enterGrill = await ctx
|
|
377
|
+
const enterGrill = await uiConfirm(ctx, confirmTitle, confirmDesc);
|
|
350
378
|
if (!enterGrill) {
|
|
351
|
-
|
|
379
|
+
// Skip grill but continue the workflow (not a cancellation)
|
|
380
|
+
return defaultResult;
|
|
352
381
|
}
|
|
353
382
|
|
|
354
383
|
// ── Step 2: Prepare output file + enhanced prompt ─────────
|
|
@@ -369,12 +398,13 @@ export async function runGrillPhase(
|
|
|
369
398
|
ctx.cwd,
|
|
370
399
|
loader.signal,
|
|
371
400
|
undefined,
|
|
372
|
-
(progress) => {
|
|
401
|
+
(progress) => {
|
|
402
|
+
const inner = (loader as unknown as { loader?: { setText?: (t: string) => void } }).loader;
|
|
403
|
+
inner?.setText?.(`🧠 ${progress.slice(0, 60)}`);
|
|
404
|
+
},
|
|
373
405
|
)
|
|
374
406
|
.then((result) => {
|
|
375
|
-
// Primary: read from file (sub-agent wrote via `write` tool)
|
|
376
407
|
let qs = readQuestionsFromFile(outputFilePath);
|
|
377
|
-
// Fallback: parse from NDJSON response text
|
|
378
408
|
if (qs.length === 0) {
|
|
379
409
|
const output = extractFinalOutput(result.output);
|
|
380
410
|
qs = parseGrillQuestions(output);
|
|
@@ -382,7 +412,6 @@ export async function runGrillPhase(
|
|
|
382
412
|
done(qs);
|
|
383
413
|
})
|
|
384
414
|
.catch(() => {
|
|
385
|
-
// On error, still try to read file (sub-agent may have written before error)
|
|
386
415
|
const qs = readQuestionsFromFile(outputFilePath);
|
|
387
416
|
done(qs);
|
|
388
417
|
});
|
|
@@ -390,12 +419,21 @@ export async function runGrillPhase(
|
|
|
390
419
|
return loader;
|
|
391
420
|
});
|
|
392
421
|
|
|
393
|
-
// ── Step 4:
|
|
394
|
-
try { fs.unlinkSync(outputFilePath); } catch { /* ignore */ }
|
|
395
|
-
|
|
396
|
-
// ── Step 4b: Retry dialog if no questions generated ──────
|
|
422
|
+
// ── Step 4: Retry dialog if no questions generated ──────
|
|
397
423
|
if (questions.length === 0) {
|
|
398
|
-
|
|
424
|
+
let failedFileContent = "";
|
|
425
|
+
let parseErrorMsg = "";
|
|
426
|
+
try {
|
|
427
|
+
if (fs.existsSync(outputFilePath)) {
|
|
428
|
+
failedFileContent = fs.readFileSync(outputFilePath, "utf-8").slice(0, 2000);
|
|
429
|
+
JSON.parse(failedFileContent);
|
|
430
|
+
}
|
|
431
|
+
} catch (e) {
|
|
432
|
+
parseErrorMsg = (e as Error).message;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const choice = await uiSelect(
|
|
436
|
+
ctx,
|
|
399
437
|
"⚠️ AI 未能成功生成评审问题",
|
|
400
438
|
[
|
|
401
439
|
"🔄 重新尝试生成评审问题",
|
|
@@ -407,8 +445,29 @@ export async function runGrillPhase(
|
|
|
407
445
|
switch (choice) {
|
|
408
446
|
case "🔄 重新尝试生成评审问题": {
|
|
409
447
|
const retryPath = grillOutputPath(ctx.cwd);
|
|
410
|
-
const
|
|
411
|
-
|
|
448
|
+
const errorFeedback = parseErrorMsg
|
|
449
|
+
? [
|
|
450
|
+
"",
|
|
451
|
+
"### ⚠️ Previous attempt had JSON errors — fix them now",
|
|
452
|
+
"",
|
|
453
|
+
`The previous attempt wrote to \`${outputFilePath}\` but the JSON was invalid.`,
|
|
454
|
+
"",
|
|
455
|
+
`JSON parse error: ${parseErrorMsg}`,
|
|
456
|
+
"",
|
|
457
|
+
"Invalid file content (first 2000 chars):",
|
|
458
|
+
"```",
|
|
459
|
+
failedFileContent.slice(0, 1000),
|
|
460
|
+
"```",
|
|
461
|
+
"",
|
|
462
|
+
"Please write valid JSON to the new path below. Make sure all strings are properly JSON-escaped.",
|
|
463
|
+
].join("\n")
|
|
464
|
+
: "";
|
|
465
|
+
const retryPrompt = [
|
|
466
|
+
assembledPrompt,
|
|
467
|
+
writeToolPromptSuffix(retryPath),
|
|
468
|
+
errorFeedback,
|
|
469
|
+
].filter(Boolean).join("\n\n");
|
|
470
|
+
const retryQuestions = await ctx.ui.custom<GrillQuestion[]>((tui, theme, _kb, done) => {
|
|
412
471
|
const loader = new BorderedLoader(tui, theme, loaderLabel);
|
|
413
472
|
loader.onAbort = () => done([]);
|
|
414
473
|
spawnSubagent(agentDef, retryPrompt, ctx.cwd, loader.signal, undefined)
|
|
@@ -420,38 +479,91 @@ export async function runGrillPhase(
|
|
|
420
479
|
.catch(() => done([]));
|
|
421
480
|
return loader;
|
|
422
481
|
});
|
|
423
|
-
|
|
424
|
-
if (questions.length === 0) {
|
|
425
|
-
ctx.ui.notify("⚠️ 再次尝试仍然失败,跳过 Grill 阶段", "warning");
|
|
482
|
+
if (retryQuestions.length === 0) {
|
|
426
483
|
return defaultResult;
|
|
427
484
|
}
|
|
428
|
-
|
|
485
|
+
// Replace questions with retry results (with back support)
|
|
486
|
+
const pairs: Array<{ question: string; answer: string }> = [];
|
|
487
|
+
let rIdx = 0;
|
|
488
|
+
while (rIdx >= 0 && rIdx < retryQuestions.length) {
|
|
489
|
+
const q = retryQuestions[rIdx]!;
|
|
490
|
+
const previousAnswer = pairs[rIdx]?.answer;
|
|
491
|
+
const answer = await showQuestionTUI(ctx, q, rIdx + 1, retryQuestions.length, qTitlePrefix,
|
|
492
|
+
rIdx > 0, previousAnswer);
|
|
493
|
+
if (answer === null) {
|
|
494
|
+
return { ...defaultResult, cancelled: true, pairs };
|
|
495
|
+
}
|
|
496
|
+
if (answer === "__BACK__") {
|
|
497
|
+
if (rIdx > 0) {
|
|
498
|
+
rIdx--;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
return { ...defaultResult, cancelled: true, pairs };
|
|
502
|
+
}
|
|
503
|
+
// Overwrite if re-answering, otherwise append
|
|
504
|
+
if (rIdx < pairs.length) {
|
|
505
|
+
pairs[rIdx] = { question: q.question, answer };
|
|
506
|
+
} else {
|
|
507
|
+
pairs.push({ question: q.question, answer });
|
|
508
|
+
}
|
|
509
|
+
rIdx++;
|
|
510
|
+
}
|
|
511
|
+
const qaBlock = pairs
|
|
512
|
+
.map((p, i) => `[评审问题 ${i + 1}]\n问题: ${p.question}\n回答: ${p.answer}`)
|
|
513
|
+
.join("\n\n");
|
|
514
|
+
const finalEnhancedPrompt = [
|
|
515
|
+
assembledPrompt,
|
|
516
|
+
"",
|
|
517
|
+
"---",
|
|
518
|
+
"## 设计评审记录",
|
|
519
|
+
"",
|
|
520
|
+
"以下是在开发前进行的设计评审问答,所有决策已确认:",
|
|
521
|
+
"",
|
|
522
|
+
qaBlock,
|
|
523
|
+
].join("\n");
|
|
524
|
+
return {
|
|
525
|
+
cancelled: false,
|
|
526
|
+
pairs,
|
|
527
|
+
enhancedPrompt: finalEnhancedPrompt,
|
|
528
|
+
};
|
|
429
529
|
}
|
|
430
530
|
case "⏭️ 跳过 Grill,直接发送 Prompt":
|
|
431
|
-
ctx.ui.notify("⏭️ 已跳过 Grill 阶段", "info");
|
|
432
531
|
return defaultResult;
|
|
433
532
|
case "❌ 取消 (Esc)":
|
|
434
533
|
default:
|
|
435
|
-
ctx.ui.notify("❌ 操作已取消", "warning");
|
|
436
534
|
return { ...defaultResult, cancelled: true };
|
|
437
535
|
}
|
|
438
536
|
}
|
|
439
537
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
// ── Step 5: TUI — present questions one by one ───────────
|
|
538
|
+
// ── Step 5: TUI — present questions one by one (with back support) ──
|
|
443
539
|
const pairs: Array<{ question: string; answer: string }> = [];
|
|
540
|
+
let qIdx = 0;
|
|
444
541
|
|
|
445
|
-
|
|
446
|
-
const q = questions[
|
|
447
|
-
const
|
|
542
|
+
while (qIdx >= 0 && qIdx < questions.length) {
|
|
543
|
+
const q = questions[qIdx]!;
|
|
544
|
+
const previousAnswer = pairs[qIdx]?.answer;
|
|
545
|
+
const answer = await showQuestionTUI(ctx, q, qIdx + 1, questions.length, qTitlePrefix,
|
|
546
|
+
qIdx > 0, previousAnswer);
|
|
448
547
|
|
|
449
548
|
if (answer === null) {
|
|
450
|
-
ctx.ui.notify("❌ 评审已取消", "warning");
|
|
451
549
|
return { ...defaultResult, cancelled: true, pairs };
|
|
452
550
|
}
|
|
453
551
|
|
|
454
|
-
|
|
552
|
+
if (answer === "__BACK__") {
|
|
553
|
+
if (qIdx > 0) {
|
|
554
|
+
qIdx--;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
return { ...defaultResult, cancelled: true, pairs };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Overwrite if re-answering (back then forward), otherwise append
|
|
561
|
+
if (qIdx < pairs.length) {
|
|
562
|
+
pairs[qIdx] = { question: q.question, answer };
|
|
563
|
+
} else {
|
|
564
|
+
pairs.push({ question: q.question, answer });
|
|
565
|
+
}
|
|
566
|
+
qIdx++;
|
|
455
567
|
}
|
|
456
568
|
|
|
457
569
|
// ── Step 6: Assemble enhanced prompt ─────────────────────
|
|
@@ -470,8 +582,6 @@ export async function runGrillPhase(
|
|
|
470
582
|
qaBlock,
|
|
471
583
|
].join("\n");
|
|
472
584
|
|
|
473
|
-
ctx.ui.notify(`✅ 评审完成,共 ${pairs.length} 道问题`, "success");
|
|
474
|
-
|
|
475
585
|
return {
|
|
476
586
|
cancelled: false,
|
|
477
587
|
pairs,
|
|
@@ -486,17 +596,32 @@ async function showQuestionTUI(
|
|
|
486
596
|
currentIndex: number,
|
|
487
597
|
totalCount: number,
|
|
488
598
|
titlePrefix = "设计方案评审",
|
|
599
|
+
backable = false,
|
|
600
|
+
previousAnswer?: string,
|
|
489
601
|
): Promise<string | null> {
|
|
490
602
|
const selectItems: SelectItem[] = q.options.map((opt, i) => ({
|
|
491
603
|
value: `opt-${i}`,
|
|
492
|
-
label:
|
|
604
|
+
label: opt === previousAnswer
|
|
605
|
+
? `(${String.fromCharCode(97 + i)}) ${opt} - 上次选择`
|
|
606
|
+
: `(${String.fromCharCode(97 + i)}) ${opt}`,
|
|
493
607
|
}));
|
|
608
|
+
|
|
609
|
+
const customLabel = previousAnswer && !q.options.includes(previousAnswer)
|
|
610
|
+
? `✏️ 自定义输入 - 上次选择`
|
|
611
|
+
: `✏️ 自定义输入`;
|
|
494
612
|
selectItems.push({
|
|
495
613
|
value: "__custom__",
|
|
496
|
-
label:
|
|
614
|
+
label: customLabel,
|
|
497
615
|
description: "输入你自己的回答,不受选项限制",
|
|
498
616
|
});
|
|
499
617
|
|
|
618
|
+
if (backable && currentIndex > 1) {
|
|
619
|
+
selectItems.push({
|
|
620
|
+
value: "__back__",
|
|
621
|
+
label: "← 返回上一题",
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
500
625
|
const title = `${titlePrefix} (问题 ${currentIndex}/${totalCount})`;
|
|
501
626
|
|
|
502
627
|
const value = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
@@ -520,8 +645,11 @@ async function showQuestionTUI(
|
|
|
520
645
|
container.addChild(selectList);
|
|
521
646
|
|
|
522
647
|
container.addChild(new Spacer(1));
|
|
648
|
+
const hint = backable && currentIndex > 1
|
|
649
|
+
? " ↑↓ 导航 • Enter 选择 • 选择←返回上一题 • Esc 取消全部评审"
|
|
650
|
+
: " ↑↓ 导航 • Enter 选择 • Esc 取消全部评审";
|
|
523
651
|
container.addChild(
|
|
524
|
-
new Text(theme.fg("dim",
|
|
652
|
+
new Text(theme.fg("dim", hint), 0, 0),
|
|
525
653
|
);
|
|
526
654
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
527
655
|
|
|
@@ -535,13 +663,18 @@ async function showQuestionTUI(
|
|
|
535
663
|
};
|
|
536
664
|
});
|
|
537
665
|
|
|
666
|
+
if (value === "__back__") return "__BACK__";
|
|
538
667
|
if (value === null) return null;
|
|
539
668
|
if (value === "__custom__") {
|
|
540
|
-
const custom = await ctx
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
669
|
+
const custom = await uiInput(ctx, "✏️ 自定义回答",
|
|
670
|
+
previousAnswer && !q.options.includes(previousAnswer)
|
|
671
|
+
? `(上次: ${previousAnswer.slice(0, 60)})`
|
|
672
|
+
: "输入你的回答内容(Esc 取消本题,回到选项)",
|
|
673
|
+
false, true,
|
|
674
|
+
previousAnswer && !q.options.includes(previousAnswer) ? previousAnswer : "",
|
|
675
|
+
);
|
|
676
|
+
if (custom === "__BACK__") return "__BACK__";
|
|
677
|
+
if (custom === undefined) return showQuestionTUI(ctx, q, currentIndex, totalCount, titlePrefix, backable, previousAnswer);
|
|
545
678
|
return custom.trim() || "(空)";
|
|
546
679
|
}
|
|
547
680
|
|
|
@@ -555,7 +688,7 @@ async function showQuestionTUI(
|
|
|
555
688
|
* Run the PRD phase:
|
|
556
689
|
* 1. Ask user if they want to create a PRD
|
|
557
690
|
* 2. Call sub-agent → gets PRD Markdown
|
|
558
|
-
* 3. Save to pi-dev-output/pi-prd/<name>.md
|
|
691
|
+
* 3. Save to .pi-dev-output/pi-prd/<name>.md
|
|
559
692
|
* 4. Ask if user wants to start development
|
|
560
693
|
*/
|
|
561
694
|
export async function runPRDPhase(
|
|
@@ -564,9 +697,10 @@ export async function runPRDPhase(
|
|
|
564
697
|
pi: ExtensionAPI,
|
|
565
698
|
ctx: ExtensionCommandContext,
|
|
566
699
|
): Promise<PRDResult | null> {
|
|
567
|
-
const wantPrd = await
|
|
700
|
+
const wantPrd = await uiConfirm(
|
|
701
|
+
ctx,
|
|
568
702
|
"📋 创建 PRD",
|
|
569
|
-
"
|
|
703
|
+
"PRD 将保存到 .pi-dev-output/pi-prd/ 目录。",
|
|
570
704
|
);
|
|
571
705
|
if (!wantPrd) return null;
|
|
572
706
|
|
|
@@ -582,7 +716,7 @@ export async function runPRDPhase(
|
|
|
582
716
|
context,
|
|
583
717
|
].join("\n");
|
|
584
718
|
|
|
585
|
-
spawnSubagent(
|
|
719
|
+
spawnSubagent(_defaultPrdAgent, prdTask, ctx.cwd, loader.signal)
|
|
586
720
|
.then((result) => {
|
|
587
721
|
const output = extractFinalOutput(result.output);
|
|
588
722
|
done(output && output.length >= 50 ? output : null);
|
|
@@ -593,7 +727,6 @@ export async function runPRDPhase(
|
|
|
593
727
|
});
|
|
594
728
|
|
|
595
729
|
if (!prdContent) {
|
|
596
|
-
ctx.ui.notify("⚠️ PRD 生成失败", "error");
|
|
597
730
|
return null;
|
|
598
731
|
}
|
|
599
732
|
|
|
@@ -602,7 +735,6 @@ export async function runPRDPhase(
|
|
|
602
735
|
const filePath = path.join(DEV_OUTPUT_DIR, PRD_DIRNAME, filename);
|
|
603
736
|
const fullPath = path.join(prdDir, filename);
|
|
604
737
|
fs.writeFileSync(fullPath, prdContent, "utf-8");
|
|
605
|
-
ctx.ui.notify(`✅ PRD 已保存到 ${filePath}`, "success");
|
|
606
738
|
|
|
607
739
|
await askDevelopmentStart(pi, ctx, prdContent, filePath);
|
|
608
740
|
return { content: prdContent, filePath };
|
|
@@ -614,7 +746,8 @@ async function askDevelopmentStart(
|
|
|
614
746
|
prdContent: string,
|
|
615
747
|
prdFilePath: string,
|
|
616
748
|
): Promise<void> {
|
|
617
|
-
const choice = await
|
|
749
|
+
const choice = await uiSelect(
|
|
750
|
+
ctx,
|
|
618
751
|
"🚀 是否开始开发?",
|
|
619
752
|
[
|
|
620
753
|
"是 — 根据 PRD 开始开发",
|
|
@@ -638,18 +771,13 @@ async function askDevelopmentStart(
|
|
|
638
771
|
"",
|
|
639
772
|
"请按照上述 PRD 逐步实现。先分析代码库结构,给出实施计划,确认后再编写代码。",
|
|
640
773
|
].join("\n");
|
|
641
|
-
pi.sendUserMessage(devMsg);
|
|
642
|
-
ctx.ui.notify("🚀 已发送开发指令给主代理", "success");
|
|
774
|
+
pi.sendUserMessage(devMsg, { deliverAs: "followUp" });
|
|
643
775
|
break;
|
|
644
776
|
}
|
|
645
777
|
case "否 — 稍后手动开始":
|
|
646
|
-
ctx.ui.notify(`📋 PRD 已保存在 ${prdFilePath},可随时手动引用`, "info");
|
|
647
778
|
break;
|
|
648
779
|
default: {
|
|
649
|
-
const customMsg = await ctx
|
|
650
|
-
placeholder: "输入你的开发指令(将结合 PRD 一起发送给主代理)",
|
|
651
|
-
required: false,
|
|
652
|
-
});
|
|
780
|
+
const customMsg = await uiInput(ctx, "✏️ 自定义开发指令", "输入你的开发指令(将结合 PRD 一起发送给主代理)");
|
|
653
781
|
if (customMsg === undefined) return askDevelopmentStart(pi, ctx, prdContent, prdFilePath);
|
|
654
782
|
const finalMsg = customMsg.trim()
|
|
655
783
|
? [
|
|
@@ -668,8 +796,7 @@ async function askDevelopmentStart(
|
|
|
668
796
|
"--- PRD 全文 ---",
|
|
669
797
|
prdContent,
|
|
670
798
|
].join("\n");
|
|
671
|
-
pi.sendUserMessage(finalMsg);
|
|
672
|
-
ctx.ui.notify("🚀 已发送自定义开发指令给主代理", "success");
|
|
799
|
+
pi.sendUserMessage(finalMsg, { deliverAs: "followUp" });
|
|
673
800
|
break;
|
|
674
801
|
}
|
|
675
802
|
}
|