pi-messenger 0.7.3

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 (45) hide show
  1. package/ARCHITECTURE.md +244 -0
  2. package/CHANGELOG.md +418 -0
  3. package/README.md +394 -0
  4. package/banner.png +0 -0
  5. package/config-overlay.ts +172 -0
  6. package/config.ts +178 -0
  7. package/crew/agents/crew-docs-scout.md +55 -0
  8. package/crew/agents/crew-gap-analyst.md +105 -0
  9. package/crew/agents/crew-github-scout.md +111 -0
  10. package/crew/agents/crew-interview-generator.md +79 -0
  11. package/crew/agents/crew-plan-sync.md +64 -0
  12. package/crew/agents/crew-practice-scout.md +62 -0
  13. package/crew/agents/crew-repo-scout.md +65 -0
  14. package/crew/agents/crew-reviewer.md +58 -0
  15. package/crew/agents/crew-web-scout.md +85 -0
  16. package/crew/agents/crew-worker.md +95 -0
  17. package/crew/agents.ts +200 -0
  18. package/crew/handlers/interview.ts +211 -0
  19. package/crew/handlers/plan.ts +358 -0
  20. package/crew/handlers/review.ts +341 -0
  21. package/crew/handlers/status.ts +257 -0
  22. package/crew/handlers/sync.ts +232 -0
  23. package/crew/handlers/task.ts +511 -0
  24. package/crew/handlers/work.ts +289 -0
  25. package/crew/id-allocator.ts +44 -0
  26. package/crew/index.ts +229 -0
  27. package/crew/state.ts +116 -0
  28. package/crew/store.ts +480 -0
  29. package/crew/types.ts +164 -0
  30. package/crew/utils/artifacts.ts +65 -0
  31. package/crew/utils/config.ts +104 -0
  32. package/crew/utils/discover.ts +170 -0
  33. package/crew/utils/install.ts +373 -0
  34. package/crew/utils/progress.ts +107 -0
  35. package/crew/utils/result.ts +16 -0
  36. package/crew/utils/truncate.ts +79 -0
  37. package/crew-overlay.ts +259 -0
  38. package/handlers.ts +799 -0
  39. package/index.ts +591 -0
  40. package/lib.ts +232 -0
  41. package/overlay.ts +687 -0
  42. package/package.json +20 -0
  43. package/skills/pi-messenger-crew/SKILL.md +140 -0
  44. package/store.ts +1068 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Crew - Plan Handler
3
+ *
4
+ * Orchestrates planning: scouts (parallel) → gap-analyst → create tasks
5
+ * Simplified: PRD → plan → tasks
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
11
+ import type { MessengerState, Dirs } from "../../lib.js";
12
+ import type { CrewParams } from "../types.js";
13
+ import { result } from "../utils/result.js";
14
+ import { spawnAgents } from "../agents.js";
15
+ import { loadCrewConfig } from "../utils/config.js";
16
+ import { discoverCrewAgents } from "../utils/discover.js";
17
+ import * as store from "../store.js";
18
+ import { getCrewDir } from "../store.js";
19
+
20
+ // Common PRD/spec file patterns to search for
21
+ const PRD_PATTERNS = [
22
+ "PRD.md", "prd.md",
23
+ "SPEC.md", "spec.md",
24
+ "REQUIREMENTS.md", "requirements.md",
25
+ "DESIGN.md", "design.md",
26
+ "PLAN.md", "plan.md",
27
+ "docs/PRD.md", "docs/prd.md",
28
+ "docs/SPEC.md", "docs/spec.md",
29
+ ];
30
+
31
+ // Scout agents to run in parallel
32
+ const SCOUT_AGENTS = [
33
+ "crew-repo-scout",
34
+ "crew-practice-scout",
35
+ "crew-docs-scout",
36
+ "crew-web-scout",
37
+ "crew-github-scout",
38
+ ];
39
+
40
+ export async function execute(
41
+ params: CrewParams,
42
+ _state: MessengerState,
43
+ _dirs: Dirs,
44
+ ctx: ExtensionContext
45
+ ) {
46
+ const cwd = ctx.cwd ?? process.cwd();
47
+ const config = loadCrewConfig(getCrewDir(cwd));
48
+ const { prd } = params;
49
+
50
+ // Check if plan already exists
51
+ const existingPlan = store.getPlan(cwd);
52
+ if (existingPlan) {
53
+ return result(`A plan already exists for ${existingPlan.prd}.\n\nTo create a new plan, first delete the existing one:\n - Delete .pi/messenger/crew/ directory\n - Or reset tasks manually`, {
54
+ mode: "plan",
55
+ error: "plan_exists",
56
+ existingPrd: existingPlan.prd
57
+ });
58
+ }
59
+
60
+ // Find PRD file
61
+ let prdPath: string;
62
+ let prdContent: string;
63
+
64
+ if (prd) {
65
+ // Explicit PRD path
66
+ prdPath = prd;
67
+ const fullPath = path.isAbsolute(prd) ? prd : path.join(cwd, prd);
68
+ if (!fs.existsSync(fullPath)) {
69
+ return result(`PRD file not found: ${prd}`, {
70
+ mode: "plan",
71
+ error: "prd_not_found",
72
+ prd
73
+ });
74
+ }
75
+ prdContent = fs.readFileSync(fullPath, "utf-8");
76
+ } else {
77
+ // Auto-discover PRD
78
+ const discovered = discoverPRD(cwd);
79
+ if (!discovered) {
80
+ return result(`No PRD file found. Create one of: ${PRD_PATTERNS.slice(0, 4).join(", ")}\n\nOr specify path: pi_messenger({ action: "plan", prd: "path/to/PRD.md" })`, {
81
+ mode: "plan",
82
+ error: "no_prd",
83
+ searchedPatterns: PRD_PATTERNS
84
+ });
85
+ }
86
+ prdPath = discovered.relativePath;
87
+ prdContent = discovered.content;
88
+ }
89
+
90
+ // Discover available scouts
91
+ const availableAgents = discoverCrewAgents(cwd);
92
+ const availableScouts = SCOUT_AGENTS.filter(name =>
93
+ availableAgents.some(a => a.name === name)
94
+ );
95
+
96
+ if (availableScouts.length === 0) {
97
+ return result("Error: No scout agents available. Run crew.install or create crew-*-scout.md agents.", {
98
+ mode: "plan",
99
+ error: "no_scouts"
100
+ });
101
+ }
102
+
103
+ // Check for gap-analyst
104
+ const hasAnalyst = availableAgents.some(a => a.name === "crew-gap-analyst");
105
+ if (!hasAnalyst) {
106
+ return result("Error: crew-gap-analyst agent not found. Required for plan synthesis.", {
107
+ mode: "plan",
108
+ error: "no_analyst"
109
+ });
110
+ }
111
+
112
+ // Create the plan entry
113
+ store.createPlan(cwd, prdPath);
114
+
115
+ // Phase 1: Run scouts in parallel
116
+ const scoutTasks = availableScouts.map(agent => ({
117
+ agent,
118
+ task: `Analyze for implementing the following PRD:
119
+
120
+ ## PRD: ${prdPath}
121
+
122
+ ${prdContent}
123
+
124
+ Provide context for planning this feature implementation.`
125
+ }));
126
+
127
+ const scoutResults = await spawnAgents(
128
+ scoutTasks,
129
+ config.concurrency.scouts,
130
+ cwd
131
+ );
132
+
133
+ // Aggregate scout findings
134
+ const scoutFindings: string[] = [];
135
+ const failedScouts: string[] = [];
136
+
137
+ for (const r of scoutResults) {
138
+ if (r.exitCode === 0 && r.output) {
139
+ scoutFindings.push(`## ${r.agent}\n\n${r.output}`);
140
+ } else {
141
+ failedScouts.push(r.agent);
142
+ }
143
+ }
144
+
145
+ if (scoutFindings.length === 0) {
146
+ // Clean up the plan entry since planning failed
147
+ store.deletePlan(cwd);
148
+ return result("Error: All scouts failed. Check agent configurations.", {
149
+ mode: "plan",
150
+ error: "all_scouts_failed",
151
+ failedScouts
152
+ });
153
+ }
154
+
155
+ // Phase 2: Run gap-analyst to synthesize findings
156
+ const aggregatedFindings = scoutFindings.join("\n\n---\n\n");
157
+
158
+ const [analystResult] = await spawnAgents([{
159
+ agent: "crew-gap-analyst",
160
+ task: `Synthesize scout findings and create task breakdown.
161
+
162
+ ## PRD: ${prdPath}
163
+
164
+ ${prdContent}
165
+
166
+ ## Scout Findings
167
+
168
+ ${aggregatedFindings}
169
+
170
+ Create a task breakdown following the exact output format specified in your instructions.`
171
+ }], 1, cwd);
172
+
173
+ if (analystResult.exitCode !== 0) {
174
+ // Clean up the plan entry since planning failed
175
+ store.deletePlan(cwd);
176
+ return result(`Error: Gap analyst failed: ${analystResult.error ?? "Unknown error"}`, {
177
+ mode: "plan",
178
+ error: "analyst_failed",
179
+ scoutResults: scoutFindings.length
180
+ });
181
+ }
182
+
183
+ // Phase 3: Parse analyst output and create tasks
184
+ const tasks = parseTasksFromOutput(analystResult.output);
185
+
186
+ if (tasks.length === 0) {
187
+ // Store the analysis as plan spec even if no tasks parsed
188
+ store.setPlanSpec(cwd, analystResult.output);
189
+
190
+ return result(`Plan analysis complete but no tasks could be parsed.\n\nAnalysis saved to plan.md. Review and create tasks manually.`, {
191
+ mode: "plan",
192
+ prd: prdPath,
193
+ analysisLength: analystResult.output.length,
194
+ scoutsRun: scoutFindings.length,
195
+ failedScouts
196
+ });
197
+ }
198
+
199
+ // Create tasks in store
200
+ const createdTasks: { id: string; title: string; dependsOn: string[] }[] = [];
201
+ const titleToId = new Map<string, string>();
202
+
203
+ // First pass: create tasks without dependencies
204
+ for (let i = 0; i < tasks.length; i++) {
205
+ const task = tasks[i];
206
+ const created = store.createTask(cwd, task.title, task.description);
207
+ createdTasks.push({ id: created.id, title: task.title, dependsOn: task.dependsOn });
208
+ titleToId.set(task.title.toLowerCase(), created.id);
209
+ // Also map "task N" format
210
+ titleToId.set(`task ${i + 1}`, created.id);
211
+ titleToId.set(`task-${i + 1}`, created.id);
212
+ }
213
+
214
+ // Second pass: resolve and update dependencies
215
+ for (const task of createdTasks) {
216
+ if (task.dependsOn.length > 0) {
217
+ const resolvedDeps: string[] = [];
218
+ for (const dep of task.dependsOn) {
219
+ const depId = titleToId.get(dep.toLowerCase());
220
+ if (depId && depId !== task.id) {
221
+ resolvedDeps.push(depId);
222
+ }
223
+ }
224
+ if (resolvedDeps.length > 0) {
225
+ store.updateTask(cwd, task.id, { depends_on: resolvedDeps });
226
+ }
227
+ }
228
+ }
229
+
230
+ // Update plan spec with full analysis
231
+ store.setPlanSpec(cwd, analystResult.output);
232
+
233
+ // Build result text
234
+ const taskList = createdTasks.map(t => {
235
+ const task = store.getTask(cwd, t.id);
236
+ const deps = task?.depends_on.length ? ` → deps: ${task.depends_on.join(", ")}` : "";
237
+ return ` - ${t.id}: ${t.title}${deps}`;
238
+ }).join("\n");
239
+
240
+ const text = `✅ Plan created from **${prdPath}**
241
+
242
+ **Scouts run:** ${scoutFindings.length}/${availableScouts.length}
243
+ ${failedScouts.length > 0 ? `**Failed scouts:** ${failedScouts.join(", ")}\n` : ""}
244
+ **Tasks created:** ${createdTasks.length}
245
+
246
+ ${taskList}
247
+
248
+ **Next steps:**
249
+ - Review tasks: \`pi_messenger({ action: "task.list" })\`
250
+ - Start work: \`pi_messenger({ action: "work" })\`
251
+ - Autonomous: \`pi_messenger({ action: "work", autonomous: true })\``;
252
+
253
+ return result(text, {
254
+ mode: "plan",
255
+ prd: prdPath,
256
+ scoutsRun: scoutFindings.length,
257
+ failedScouts,
258
+ tasksCreated: createdTasks.map(t => ({ id: t.id, title: t.title }))
259
+ });
260
+ }
261
+
262
+ // =============================================================================
263
+ // Task Parsing
264
+ // =============================================================================
265
+
266
+ interface ParsedTask {
267
+ title: string;
268
+ description: string;
269
+ dependsOn: string[];
270
+ }
271
+
272
+ /**
273
+ * Parses tasks from gap-analyst output.
274
+ *
275
+ * Expected format:
276
+ * ### Task 1: [Title]
277
+ * [Description...]
278
+ * Dependencies: none | Task 1, Task 2
279
+ */
280
+ function parseTasksFromOutput(output: string): ParsedTask[] {
281
+ const tasks: ParsedTask[] = [];
282
+
283
+ // Match task blocks
284
+ const taskRegex = /###\s*Task\s*\d+:\s*(.+?)\n([\s\S]*?)(?=###\s*Task\s*\d+:|## |$)/gi;
285
+ let match;
286
+
287
+ while ((match = taskRegex.exec(output)) !== null) {
288
+ const title = match[1].trim();
289
+ const body = match[2].trim();
290
+
291
+ // Extract dependencies
292
+ const depsMatch = body.match(/Dependencies?:\s*(.+?)(?:\n|$)/i);
293
+ let dependsOn: string[] = [];
294
+
295
+ if (depsMatch) {
296
+ const depsText = depsMatch[1].trim().toLowerCase();
297
+ if (depsText !== "none" && depsText !== "n/a" && depsText !== "-") {
298
+ // Parse "Task 1, Task 2" or "task-1, task-2" format
299
+ dependsOn = depsText
300
+ .split(/,\s*/)
301
+ .map(d => d.trim())
302
+ .filter(d => d.length > 0);
303
+ }
304
+ }
305
+
306
+ // Description is everything except the dependencies line
307
+ const description = body
308
+ .replace(/Dependencies?:\s*.+?(?:\n|$)/i, "")
309
+ .trim();
310
+
311
+ tasks.push({ title, description, dependsOn });
312
+ }
313
+
314
+ return tasks;
315
+ }
316
+
317
+ // =============================================================================
318
+ // PRD Discovery
319
+ // =============================================================================
320
+
321
+ interface DiscoveredPRD {
322
+ relativePath: string;
323
+ content: string;
324
+ }
325
+
326
+ const MAX_PRD_SIZE = 100000; // 100KB max
327
+
328
+ /**
329
+ * Discovers PRD file from the project.
330
+ */
331
+ function discoverPRD(cwd: string): DiscoveredPRD | null {
332
+ const seenPaths = new Set<string>();
333
+
334
+ for (const pattern of PRD_PATTERNS) {
335
+ const filePath = path.join(cwd, pattern);
336
+ if (fs.existsSync(filePath)) {
337
+ try {
338
+ // Use realpath to handle case-insensitive filesystems
339
+ const realPath = fs.realpathSync(filePath);
340
+ if (seenPaths.has(realPath)) continue;
341
+ seenPaths.add(realPath);
342
+
343
+ let content = fs.readFileSync(filePath, "utf-8");
344
+
345
+ // Truncate if too large
346
+ if (content.length > MAX_PRD_SIZE) {
347
+ content = content.slice(0, MAX_PRD_SIZE) + "\n\n[Content truncated]";
348
+ }
349
+
350
+ return { relativePath: pattern, content };
351
+ } catch {
352
+ // Ignore read errors
353
+ }
354
+ }
355
+ }
356
+
357
+ return null;
358
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Crew - Review Handler
3
+ *
4
+ * Spawns reviewer with git diff context for task or plan review.
5
+ * Simplified: works with current plan
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
10
+ import type { MessengerState, Dirs } from "../../lib.js";
11
+ import type { CrewParams } from "../types.js";
12
+ import { result } from "../utils/result.js";
13
+ import { spawnAgents } from "../agents.js";
14
+ import { discoverCrewAgents } from "../utils/discover.js";
15
+ import * as store from "../store.js";
16
+
17
+ export async function execute(
18
+ params: CrewParams,
19
+ _state: MessengerState,
20
+ _dirs: Dirs,
21
+ ctx: ExtensionContext
22
+ ) {
23
+ const cwd = ctx.cwd ?? process.cwd();
24
+ const { target, type } = params;
25
+
26
+ if (!target) {
27
+ return result("Error: target (task ID) required for review action.\n\nUsage: pi_messenger({ action: \"review\", target: \"task-1\" })", {
28
+ mode: "review",
29
+ error: "missing_target"
30
+ });
31
+ }
32
+
33
+ // Check for reviewer agent
34
+ const availableAgents = discoverCrewAgents(cwd);
35
+ const hasReviewer = availableAgents.some(a => a.name === "crew-reviewer");
36
+ if (!hasReviewer) {
37
+ return result("Error: crew-reviewer agent not found. Required for code review.", {
38
+ mode: "review",
39
+ error: "no_reviewer"
40
+ });
41
+ }
42
+
43
+ // Determine review type: "impl" for task, "plan" for plan review
44
+ const reviewType = type ?? (target.startsWith("task-") ? "impl" : "plan");
45
+
46
+ if (reviewType === "impl") {
47
+ return reviewImplementation(cwd, target);
48
+ } else {
49
+ return reviewPlan(cwd);
50
+ }
51
+ }
52
+
53
+ // =============================================================================
54
+ // Implementation Review
55
+ // =============================================================================
56
+
57
+ async function reviewImplementation(cwd: string, taskId: string) {
58
+ const task = store.getTask(cwd, taskId);
59
+ if (!task) {
60
+ return result(`Error: Task ${taskId} not found.`, {
61
+ mode: "review",
62
+ error: "task_not_found",
63
+ target: taskId
64
+ });
65
+ }
66
+
67
+ if (task.status !== "done" && task.status !== "in_progress") {
68
+ return result(`Error: Task ${taskId} is ${task.status}. Can only review in_progress or done tasks.`, {
69
+ mode: "review",
70
+ error: "invalid_status",
71
+ status: task.status
72
+ });
73
+ }
74
+
75
+ // Get git diff
76
+ const baseCommit = task.base_commit;
77
+ if (!baseCommit) {
78
+ return result(`Error: Task ${taskId} has no base_commit. Cannot generate diff.`, {
79
+ mode: "review",
80
+ error: "no_base_commit"
81
+ });
82
+ }
83
+
84
+ const diff = getGitDiff(baseCommit, cwd);
85
+ const commitLog = getCommitLog(baseCommit, cwd);
86
+
87
+ // Get task spec for context
88
+ const taskSpec = store.getTaskSpec(cwd, taskId) ?? "";
89
+ const plan = store.getPlan(cwd);
90
+
91
+ // Build review prompt
92
+ const prompt = `# Code Review Request
93
+
94
+ ## Task Information
95
+
96
+ **Task ID:** ${taskId}
97
+ **Task Title:** ${task.title}
98
+ **PRD:** ${plan?.prd ?? "Unknown"}
99
+
100
+ ## Task Specification
101
+
102
+ ${taskSpec || "*No spec available*"}
103
+
104
+ ## Changes
105
+
106
+ ### Commits
107
+ ${commitLog || "*No commits*"}
108
+
109
+ ### Diff
110
+ \`\`\`diff
111
+ ${diff}
112
+ \`\`\`
113
+
114
+ ## Your Review
115
+
116
+ Review this implementation following the crew-reviewer protocol.
117
+ Output your verdict as SHIP, NEEDS_WORK, or MAJOR_RETHINK with detailed feedback.`;
118
+
119
+ // Spawn reviewer
120
+ const [reviewResult] = await spawnAgents([{
121
+ agent: "crew-reviewer",
122
+ task: prompt
123
+ }], 1, cwd);
124
+
125
+ if (reviewResult.exitCode !== 0) {
126
+ return result(`Error: Reviewer failed: ${reviewResult.error ?? "Unknown error"}`, {
127
+ mode: "review",
128
+ error: "reviewer_failed"
129
+ });
130
+ }
131
+
132
+ // Parse verdict from output
133
+ const verdict = parseVerdict(reviewResult.output);
134
+
135
+ // Store review feedback in task for retry context
136
+ store.updateTask(cwd, taskId, {
137
+ last_review: {
138
+ verdict: verdict.verdict,
139
+ summary: verdict.summary,
140
+ issues: verdict.issues,
141
+ suggestions: verdict.suggestions,
142
+ reviewed_at: new Date().toISOString()
143
+ }
144
+ });
145
+
146
+ const text = `# Review: ${taskId}
147
+
148
+ **Verdict:** ${verdict.verdict}
149
+
150
+ ${verdict.summary}
151
+
152
+ ${verdict.issues.length > 0 ? `## Issues\n${verdict.issues.map(i => `- ${i}`).join("\n")}` : ""}
153
+
154
+ ${verdict.suggestions.length > 0 ? `## Suggestions\n${verdict.suggestions.map(s => `- ${s}`).join("\n")}` : ""}
155
+
156
+ ${verdict.verdict === "SHIP" ? "✅ Ready to merge!" : verdict.verdict === "NEEDS_WORK" ? "⚠️ Address issues and re-review." : "🔄 Consider re-planning this task."}`;
157
+
158
+ return result(text, {
159
+ mode: "review",
160
+ type: "impl",
161
+ taskId,
162
+ verdict: verdict.verdict,
163
+ issueCount: verdict.issues.length,
164
+ suggestionCount: verdict.suggestions.length
165
+ });
166
+ }
167
+
168
+ // =============================================================================
169
+ // Plan Review
170
+ // =============================================================================
171
+
172
+ async function reviewPlan(cwd: string) {
173
+ const plan = store.getPlan(cwd);
174
+ if (!plan) {
175
+ return result("Error: No plan found.", {
176
+ mode: "review",
177
+ error: "no_plan"
178
+ });
179
+ }
180
+
181
+ const planSpec = store.getPlanSpec(cwd);
182
+ const tasks = store.getTasks(cwd);
183
+
184
+ // Build task overview
185
+ const taskOverview = tasks.map(t => {
186
+ const spec = store.getTaskSpec(cwd, t.id);
187
+ const deps = t.depends_on.length > 0 ? ` (deps: ${t.depends_on.join(", ")})` : "";
188
+ const specPreview = spec && !spec.includes("*Spec pending*")
189
+ ? `\n ${spec.slice(0, 200)}${spec.length > 200 ? "..." : ""}`
190
+ : "";
191
+ return `- ${t.id}: ${t.title}${deps}${specPreview}`;
192
+ }).join("\n");
193
+
194
+ // Build review prompt
195
+ const prompt = `# Plan Review Request
196
+
197
+ ## Plan Information
198
+
199
+ **PRD:** ${plan.prd}
200
+ **Tasks:** ${tasks.length}
201
+ **Progress:** ${plan.completed_count}/${plan.task_count}
202
+
203
+ ## Plan Specification
204
+
205
+ ${planSpec || "*No spec available*"}
206
+
207
+ ## Task Breakdown
208
+
209
+ ${taskOverview || "*No tasks*"}
210
+
211
+ ## Your Review
212
+
213
+ Review this plan for:
214
+ 1. Completeness - Are all requirements covered?
215
+ 2. Task granularity - Are tasks appropriately sized?
216
+ 3. Dependencies - Are dependencies correct and complete?
217
+ 4. Gaps - Are there missing tasks or edge cases?
218
+ 5. Order - Is the execution order optimal?
219
+
220
+ Output your verdict as SHIP (plan is solid), NEEDS_WORK (minor adjustments), or MAJOR_RETHINK (fundamental issues).`;
221
+
222
+ // Spawn reviewer
223
+ const [reviewResult] = await spawnAgents([{
224
+ agent: "crew-reviewer",
225
+ task: prompt
226
+ }], 1, cwd);
227
+
228
+ if (reviewResult.exitCode !== 0) {
229
+ return result(`Error: Reviewer failed: ${reviewResult.error ?? "Unknown error"}`, {
230
+ mode: "review",
231
+ error: "reviewer_failed"
232
+ });
233
+ }
234
+
235
+ // Parse verdict
236
+ const verdict = parseVerdict(reviewResult.output);
237
+
238
+ const text = `# Plan Review
239
+
240
+ **PRD:** ${plan.prd}
241
+ **Verdict:** ${verdict.verdict}
242
+
243
+ ${verdict.summary}
244
+
245
+ ${verdict.issues.length > 0 ? `## Issues\n${verdict.issues.map(i => `- ${i}`).join("\n")}` : ""}
246
+
247
+ ${verdict.suggestions.length > 0 ? `## Suggestions\n${verdict.suggestions.map(s => `- ${s}`).join("\n")}` : ""}
248
+
249
+ ${verdict.verdict === "SHIP" ? "✅ Plan is ready for execution!" : verdict.verdict === "NEEDS_WORK" ? "⚠️ Adjust plan before starting work." : "🔄 Consider re-planning with more context."}`;
250
+
251
+ return result(text, {
252
+ mode: "review",
253
+ type: "plan",
254
+ prd: plan.prd,
255
+ verdict: verdict.verdict,
256
+ issueCount: verdict.issues.length,
257
+ suggestionCount: verdict.suggestions.length
258
+ });
259
+ }
260
+
261
+ // =============================================================================
262
+ // Helpers
263
+ // =============================================================================
264
+
265
+ function getGitDiff(baseCommit: string, cwd: string): string {
266
+ try {
267
+ const diff = execSync(
268
+ `git diff ${baseCommit}..HEAD`,
269
+ { cwd, encoding: "utf-8", maxBuffer: 5 * 1024 * 1024 }
270
+ );
271
+ // Truncate very long diffs
272
+ if (diff.length > 50000) {
273
+ return diff.slice(0, 50000) + "\n\n[Diff truncated - too large]";
274
+ }
275
+ return diff || "*No changes*";
276
+ } catch (err) {
277
+ const message = err instanceof Error ? err.message : "Unknown error";
278
+ return `*Failed to get diff: ${message}*`;
279
+ }
280
+ }
281
+
282
+ function getCommitLog(baseCommit: string, cwd: string): string {
283
+ try {
284
+ return execSync(
285
+ `git log ${baseCommit}..HEAD --oneline --no-decorate`,
286
+ { cwd, encoding: "utf-8" }
287
+ ).trim() || "*No commits*";
288
+ } catch {
289
+ return "*No commits*";
290
+ }
291
+ }
292
+
293
+ interface ParsedReview {
294
+ verdict: "SHIP" | "NEEDS_WORK" | "MAJOR_RETHINK";
295
+ summary: string;
296
+ issues: string[];
297
+ suggestions: string[];
298
+ }
299
+
300
+ function parseVerdict(output: string): ParsedReview {
301
+ const result: ParsedReview = {
302
+ verdict: "NEEDS_WORK",
303
+ summary: "",
304
+ issues: [],
305
+ suggestions: []
306
+ };
307
+
308
+ // Extract verdict
309
+ const verdictMatch = output.match(/##\s*Verdict:\s*(SHIP|NEEDS_WORK|MAJOR_RETHINK)/i);
310
+ if (verdictMatch) {
311
+ result.verdict = verdictMatch[1].toUpperCase() as ParsedReview["verdict"];
312
+ }
313
+
314
+ // Extract summary (text between Verdict and next ##)
315
+ const summaryMatch = output.match(/##\s*Verdict:.*?\n([\s\S]*?)(?=\n##|$)/i);
316
+ if (summaryMatch) {
317
+ result.summary = summaryMatch[1].trim();
318
+ }
319
+
320
+ // Extract issues
321
+ const issuesMatch = output.match(/##\s*Issues?\s*\n([\s\S]*?)(?=\n##|$)/i);
322
+ if (issuesMatch) {
323
+ result.issues = issuesMatch[1]
324
+ .split("\n")
325
+ .filter(line => line.trim().startsWith("-") || line.trim().startsWith("*"))
326
+ .map(line => line.replace(/^[\s\-*]+/, "").trim())
327
+ .filter(Boolean);
328
+ }
329
+
330
+ // Extract suggestions
331
+ const suggestionsMatch = output.match(/##\s*Suggestions?\s*\n([\s\S]*?)(?=\n##|$)/i);
332
+ if (suggestionsMatch) {
333
+ result.suggestions = suggestionsMatch[1]
334
+ .split("\n")
335
+ .filter(line => line.trim().startsWith("-") || line.trim().startsWith("*"))
336
+ .map(line => line.replace(/^[\s\-*]+/, "").trim())
337
+ .filter(Boolean);
338
+ }
339
+
340
+ return result;
341
+ }