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.
- package/ARCHITECTURE.md +244 -0
- package/CHANGELOG.md +418 -0
- package/README.md +394 -0
- package/banner.png +0 -0
- package/config-overlay.ts +172 -0
- package/config.ts +178 -0
- package/crew/agents/crew-docs-scout.md +55 -0
- package/crew/agents/crew-gap-analyst.md +105 -0
- package/crew/agents/crew-github-scout.md +111 -0
- package/crew/agents/crew-interview-generator.md +79 -0
- package/crew/agents/crew-plan-sync.md +64 -0
- package/crew/agents/crew-practice-scout.md +62 -0
- package/crew/agents/crew-repo-scout.md +65 -0
- package/crew/agents/crew-reviewer.md +58 -0
- package/crew/agents/crew-web-scout.md +85 -0
- package/crew/agents/crew-worker.md +95 -0
- package/crew/agents.ts +200 -0
- package/crew/handlers/interview.ts +211 -0
- package/crew/handlers/plan.ts +358 -0
- package/crew/handlers/review.ts +341 -0
- package/crew/handlers/status.ts +257 -0
- package/crew/handlers/sync.ts +232 -0
- package/crew/handlers/task.ts +511 -0
- package/crew/handlers/work.ts +289 -0
- package/crew/id-allocator.ts +44 -0
- package/crew/index.ts +229 -0
- package/crew/state.ts +116 -0
- package/crew/store.ts +480 -0
- package/crew/types.ts +164 -0
- package/crew/utils/artifacts.ts +65 -0
- package/crew/utils/config.ts +104 -0
- package/crew/utils/discover.ts +170 -0
- package/crew/utils/install.ts +373 -0
- package/crew/utils/progress.ts +107 -0
- package/crew/utils/result.ts +16 -0
- package/crew/utils/truncate.ts +79 -0
- package/crew-overlay.ts +259 -0
- package/handlers.ts +799 -0
- package/index.ts +591 -0
- package/lib.ts +232 -0
- package/overlay.ts +687 -0
- package/package.json +20 -0
- package/skills/pi-messenger-crew/SKILL.md +140 -0
- package/store.ts +1068 -0
- 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
|
+
}
|