pi-superteam 0.1.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/CHANGELOG.md +37 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/agents/architect.md +45 -0
- package/agents/implementer.md +40 -0
- package/agents/performance-reviewer.md +51 -0
- package/agents/quality-reviewer.md +53 -0
- package/agents/scout.md +24 -0
- package/agents/security-reviewer.md +52 -0
- package/agents/spec-reviewer.md +46 -0
- package/docs/guides/agents.md +200 -0
- package/docs/guides/configuration.md +164 -0
- package/docs/guides/rules.md +91 -0
- package/docs/guides/sdd-workflow.md +173 -0
- package/docs/guides/tdd-guard.md +144 -0
- package/package.json +53 -0
- package/prompts/implement.md +9 -0
- package/prompts/review-parallel.md +11 -0
- package/prompts/scout.md +8 -0
- package/prompts/sdd.md +9 -0
- package/rules/no-impl-before-spec.md +17 -0
- package/rules/test-first.md +11 -0
- package/rules/yagni.md +11 -0
- package/skills/acceptance-test-driven-development/SKILL.md +60 -0
- package/skills/brainstorming/SKILL.md +49 -0
- package/skills/subagent-driven-development/SKILL.md +86 -0
- package/skills/test-driven-development/SKILL.md +97 -0
- package/skills/writing-plans/SKILL.md +65 -0
- package/src/config.ts +181 -0
- package/src/dispatch.ts +567 -0
- package/src/index.ts +721 -0
- package/src/review-parser.ts +212 -0
- package/src/rules/engine.ts +215 -0
- package/src/workflow/sdd.ts +379 -0
- package/src/workflow/state.ts +422 -0
- package/src/workflow/tdd-guard.ts +516 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDD Orchestrator — Subagent-Driven Development loop.
|
|
3
|
+
*
|
|
4
|
+
* For each task in a plan:
|
|
5
|
+
* 1. Dispatch implementer
|
|
6
|
+
* 2. Compute changed files via git diff
|
|
7
|
+
* 3. Run required reviews (spec, quality) sequentially
|
|
8
|
+
* 4. Run optional reviews (security, performance) in parallel
|
|
9
|
+
* 5. On failure: dispatch implementer to fix, re-review
|
|
10
|
+
* 6. On max iterations or inconclusive: escalate to human
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { getConfig } from "../config.js";
|
|
16
|
+
import {
|
|
17
|
+
type AgentProfile,
|
|
18
|
+
type DispatchResult,
|
|
19
|
+
aggregateUsage,
|
|
20
|
+
checkCostBudget,
|
|
21
|
+
discoverAgents,
|
|
22
|
+
dispatchAgent,
|
|
23
|
+
dispatchParallel,
|
|
24
|
+
formatUsage,
|
|
25
|
+
getFinalOutput,
|
|
26
|
+
} from "../dispatch.js";
|
|
27
|
+
import {
|
|
28
|
+
type ParseResult,
|
|
29
|
+
type ReviewFindings,
|
|
30
|
+
formatFindings,
|
|
31
|
+
hasCriticalFindings,
|
|
32
|
+
parseReviewOutput,
|
|
33
|
+
} from "../review-parser.js";
|
|
34
|
+
import {
|
|
35
|
+
type PlanTask,
|
|
36
|
+
addCostToState,
|
|
37
|
+
addReviewCycle,
|
|
38
|
+
getCurrentTask,
|
|
39
|
+
getState,
|
|
40
|
+
incrementFixAttempts,
|
|
41
|
+
updateTaskStatus,
|
|
42
|
+
updateWidget,
|
|
43
|
+
} from "./state.js";
|
|
44
|
+
|
|
45
|
+
// --- Types ---
|
|
46
|
+
|
|
47
|
+
export interface SddResult {
|
|
48
|
+
taskId: number;
|
|
49
|
+
taskTitle: string;
|
|
50
|
+
status: "complete" | "escalated" | "aborted";
|
|
51
|
+
reviewResults: ReviewResult[];
|
|
52
|
+
totalUsage: ReturnType<typeof aggregateUsage>;
|
|
53
|
+
escalationReason?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ReviewResult {
|
|
57
|
+
reviewType: string;
|
|
58
|
+
agent: string;
|
|
59
|
+
parseResult: ParseResult;
|
|
60
|
+
iteration: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Orchestrator ---
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run the SDD loop for the current task.
|
|
67
|
+
* Returns result for the task.
|
|
68
|
+
*/
|
|
69
|
+
export async function runSddTask(
|
|
70
|
+
ctx: ExtensionContext,
|
|
71
|
+
signal?: AbortSignal,
|
|
72
|
+
onStatus?: (msg: string) => void,
|
|
73
|
+
): Promise<SddResult> {
|
|
74
|
+
const task = getCurrentTask();
|
|
75
|
+
if (!task) {
|
|
76
|
+
return {
|
|
77
|
+
taskId: -1,
|
|
78
|
+
taskTitle: "(no task)",
|
|
79
|
+
status: "aborted",
|
|
80
|
+
reviewResults: [],
|
|
81
|
+
totalUsage: aggregateUsage([]),
|
|
82
|
+
escalationReason: "No current task. Load a plan with /sdd load <file>.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const config = getConfig(ctx.cwd);
|
|
87
|
+
const { agents } = discoverAgents(ctx.cwd, true);
|
|
88
|
+
const allResults: DispatchResult[] = [];
|
|
89
|
+
const reviewResults: ReviewResult[] = [];
|
|
90
|
+
|
|
91
|
+
const notify = (msg: string) => {
|
|
92
|
+
onStatus?.(msg);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const findAgent = (name: string): AgentProfile | undefined =>
|
|
96
|
+
agents.find((a) => a.name === name);
|
|
97
|
+
|
|
98
|
+
// --- Step 1: Implement ---
|
|
99
|
+
notify(`Task ${task.id}: "${task.title}" — implementing...`);
|
|
100
|
+
updateTaskStatus(task.id, "implementing");
|
|
101
|
+
updateWidget(ctx);
|
|
102
|
+
|
|
103
|
+
const implementer = findAgent("implementer");
|
|
104
|
+
if (!implementer) {
|
|
105
|
+
return escalate(task, allResults, reviewResults, "No implementer agent found.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Cost check
|
|
109
|
+
const costCheck = checkCostBudget(ctx.cwd);
|
|
110
|
+
if (!costCheck.allowed) {
|
|
111
|
+
return escalate(task, allResults, reviewResults, costCheck.warning!);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Snapshot files before implementation
|
|
115
|
+
const filesBefore = getTrackedFiles(ctx.cwd);
|
|
116
|
+
|
|
117
|
+
const implTask = buildImplTask(task);
|
|
118
|
+
const implResult = await dispatchAgent(implementer, implTask, ctx.cwd, signal);
|
|
119
|
+
allResults.push(implResult);
|
|
120
|
+
addCostToState(implResult.usage.cost);
|
|
121
|
+
|
|
122
|
+
if (implResult.exitCode !== 0) {
|
|
123
|
+
return escalate(task, allResults, reviewResults,
|
|
124
|
+
`Implementer failed (exit ${implResult.exitCode}): ${implResult.errorMessage || "unknown error"}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Compute changed files
|
|
128
|
+
const filesAfter = getTrackedFiles(ctx.cwd);
|
|
129
|
+
const changedFiles = computeChangedFiles(filesBefore, filesAfter, ctx.cwd);
|
|
130
|
+
const changedFilesList = changedFiles.length > 0 ? changedFiles.join(", ") : "(no tracked changes)";
|
|
131
|
+
|
|
132
|
+
// --- Step 2: Required reviews ---
|
|
133
|
+
updateTaskStatus(task.id, "reviewing");
|
|
134
|
+
updateWidget(ctx);
|
|
135
|
+
|
|
136
|
+
for (const reviewType of config.review.required) {
|
|
137
|
+
const agentName = `${reviewType}-reviewer`;
|
|
138
|
+
const reviewer = findAgent(agentName);
|
|
139
|
+
if (!reviewer) {
|
|
140
|
+
notify(`Warning: ${agentName} not found, skipping.`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let iteration = 0;
|
|
145
|
+
let passed = false;
|
|
146
|
+
|
|
147
|
+
while (iteration < config.review.maxIterations && !passed) {
|
|
148
|
+
iteration++;
|
|
149
|
+
notify(`Task ${task.id}: ${reviewType} review (attempt ${iteration}/${config.review.maxIterations})...`);
|
|
150
|
+
|
|
151
|
+
const costCheck = checkCostBudget(ctx.cwd);
|
|
152
|
+
if (!costCheck.allowed) {
|
|
153
|
+
return escalate(task, allResults, reviewResults, costCheck.warning!);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const reviewTask = buildReviewTask(task, reviewType, changedFilesList);
|
|
157
|
+
const reviewResult = await dispatchAgent(reviewer, reviewTask, ctx.cwd, signal);
|
|
158
|
+
allResults.push(reviewResult);
|
|
159
|
+
addCostToState(reviewResult.usage.cost);
|
|
160
|
+
|
|
161
|
+
const output = getFinalOutput(reviewResult.messages);
|
|
162
|
+
const parsed = parseReviewOutput(output);
|
|
163
|
+
|
|
164
|
+
reviewResults.push({ reviewType, agent: agentName, parseResult: parsed, iteration });
|
|
165
|
+
|
|
166
|
+
addReviewCycle({
|
|
167
|
+
taskId: task.id,
|
|
168
|
+
reviewType,
|
|
169
|
+
agent: agentName,
|
|
170
|
+
status: parsed.status === "pass" ? "passed" : parsed.status === "fail" ? "failed" : "inconclusive",
|
|
171
|
+
findings: parsed.status !== "inconclusive" ? (parsed.findings as any) : undefined,
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
});
|
|
174
|
+
updateWidget(ctx);
|
|
175
|
+
|
|
176
|
+
if (parsed.status === "pass") {
|
|
177
|
+
passed = true;
|
|
178
|
+
notify(`Task ${task.id}: ${reviewType} review PASSED`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (parsed.status === "inconclusive") {
|
|
183
|
+
if (config.review.escalateOnMaxIterations) {
|
|
184
|
+
return escalate(task, allResults, reviewResults,
|
|
185
|
+
`${reviewType} review produced inconclusive output (no valid JSON).`);
|
|
186
|
+
}
|
|
187
|
+
notify(`Task ${task.id}: ${reviewType} review inconclusive, skipping.`);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Failed — try to fix
|
|
192
|
+
if (iteration < config.review.maxIterations) {
|
|
193
|
+
notify(`Task ${task.id}: ${reviewType} review FAILED, dispatching fix (attempt ${iteration + 1})...`);
|
|
194
|
+
updateTaskStatus(task.id, "fixing");
|
|
195
|
+
incrementFixAttempts(task.id);
|
|
196
|
+
updateWidget(ctx);
|
|
197
|
+
|
|
198
|
+
const fixTask = buildFixTask(task, reviewType, parsed.findings, changedFilesList);
|
|
199
|
+
const fixResult = await dispatchAgent(implementer, fixTask, ctx.cwd, signal);
|
|
200
|
+
allResults.push(fixResult);
|
|
201
|
+
addCostToState(fixResult.usage.cost);
|
|
202
|
+
|
|
203
|
+
if (fixResult.exitCode !== 0) {
|
|
204
|
+
return escalate(task, allResults, reviewResults,
|
|
205
|
+
`Fix attempt failed (exit ${fixResult.exitCode}): ${fixResult.errorMessage || "unknown"}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
updateTaskStatus(task.id, "reviewing");
|
|
209
|
+
updateWidget(ctx);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!passed && config.review.escalateOnMaxIterations) {
|
|
214
|
+
const lastReview = reviewResults[reviewResults.length - 1];
|
|
215
|
+
const reason = lastReview?.parseResult.status === "fail"
|
|
216
|
+
? `${reviewType} review failed after ${config.review.maxIterations} attempts.\n${formatFindings(lastReview.parseResult.findings, reviewType)}`
|
|
217
|
+
: `${reviewType} review did not pass after ${config.review.maxIterations} attempts.`;
|
|
218
|
+
return escalate(task, allResults, reviewResults, reason);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- Step 3: Optional parallel reviews ---
|
|
223
|
+
if (config.review.optional.length > 0) {
|
|
224
|
+
const optionalAgents: AgentProfile[] = [];
|
|
225
|
+
const optionalTasks: string[] = [];
|
|
226
|
+
|
|
227
|
+
for (const reviewType of config.review.optional) {
|
|
228
|
+
const agentName = `${reviewType}-reviewer`;
|
|
229
|
+
const reviewer = findAgent(agentName);
|
|
230
|
+
if (reviewer) {
|
|
231
|
+
optionalAgents.push(reviewer);
|
|
232
|
+
optionalTasks.push(buildReviewTask(task, reviewType, changedFilesList));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (optionalAgents.length > 0) {
|
|
237
|
+
const costCheck = checkCostBudget(ctx.cwd);
|
|
238
|
+
if (costCheck.allowed) {
|
|
239
|
+
notify(`Task ${task.id}: running optional reviews (${config.review.optional.join(", ")})...`);
|
|
240
|
+
|
|
241
|
+
if (config.review.parallelOptional && optionalAgents.length > 1) {
|
|
242
|
+
const optResults = await dispatchParallel(optionalAgents, optionalTasks, ctx.cwd, signal);
|
|
243
|
+
allResults.push(...optResults);
|
|
244
|
+
for (const r of optResults) addCostToState(r.usage.cost);
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < optResults.length; i++) {
|
|
247
|
+
const output = getFinalOutput(optResults[i].messages);
|
|
248
|
+
const parsed = parseReviewOutput(output);
|
|
249
|
+
const reviewType = config.review.optional[i];
|
|
250
|
+
reviewResults.push({ reviewType, agent: optionalAgents[i].name, parseResult: parsed, iteration: 1 });
|
|
251
|
+
|
|
252
|
+
addReviewCycle({
|
|
253
|
+
taskId: task.id,
|
|
254
|
+
reviewType,
|
|
255
|
+
agent: optionalAgents[i].name,
|
|
256
|
+
status: parsed.status === "pass" ? "passed" : parsed.status === "fail" ? "failed" : "inconclusive",
|
|
257
|
+
findings: parsed.status !== "inconclusive" ? (parsed.findings as any) : undefined,
|
|
258
|
+
timestamp: Date.now(),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
for (let i = 0; i < optionalAgents.length; i++) {
|
|
263
|
+
const result = await dispatchAgent(optionalAgents[i], optionalTasks[i], ctx.cwd, signal);
|
|
264
|
+
allResults.push(result);
|
|
265
|
+
addCostToState(result.usage.cost);
|
|
266
|
+
|
|
267
|
+
const output = getFinalOutput(result.messages);
|
|
268
|
+
const parsed = parseReviewOutput(output);
|
|
269
|
+
const reviewType = config.review.optional[i];
|
|
270
|
+
reviewResults.push({ reviewType, agent: optionalAgents[i].name, parseResult: parsed, iteration: 1 });
|
|
271
|
+
|
|
272
|
+
addReviewCycle({
|
|
273
|
+
taskId: task.id,
|
|
274
|
+
reviewType,
|
|
275
|
+
agent: optionalAgents[i].name,
|
|
276
|
+
status: parsed.status === "pass" ? "passed" : parsed.status === "fail" ? "failed" : "inconclusive",
|
|
277
|
+
findings: parsed.status !== "inconclusive" ? (parsed.findings as any) : undefined,
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check for critical findings in optional reviews
|
|
284
|
+
for (const rr of reviewResults.filter((r) => config.review.optional.includes(r.reviewType))) {
|
|
285
|
+
if (rr.parseResult.status === "fail" && hasCriticalFindings(rr.parseResult.findings)) {
|
|
286
|
+
notify(`Task ${task.id}: critical findings in ${rr.reviewType} review — escalating`);
|
|
287
|
+
return escalate(task, allResults, reviewResults,
|
|
288
|
+
`Critical findings in optional ${rr.reviewType} review:\n${formatFindings(rr.parseResult.findings, rr.reviewType)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
updateWidget(ctx);
|
|
293
|
+
} else {
|
|
294
|
+
notify(`Skipping optional reviews: ${costCheck.warning}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Step 4: Mark complete ---
|
|
300
|
+
updateTaskStatus(task.id, "complete");
|
|
301
|
+
updateWidget(ctx);
|
|
302
|
+
notify(`Task ${task.id}: "${task.title}" — COMPLETE`);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
taskId: task.id,
|
|
306
|
+
taskTitle: task.title,
|
|
307
|
+
status: "complete",
|
|
308
|
+
reviewResults,
|
|
309
|
+
totalUsage: aggregateUsage(allResults),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- Helpers ---
|
|
314
|
+
|
|
315
|
+
function escalate(
|
|
316
|
+
task: PlanTask,
|
|
317
|
+
allResults: DispatchResult[],
|
|
318
|
+
reviewResults: ReviewResult[],
|
|
319
|
+
reason: string,
|
|
320
|
+
): SddResult {
|
|
321
|
+
updateTaskStatus(task.id, "fixing");
|
|
322
|
+
return {
|
|
323
|
+
taskId: task.id,
|
|
324
|
+
taskTitle: task.title,
|
|
325
|
+
status: "escalated",
|
|
326
|
+
reviewResults,
|
|
327
|
+
totalUsage: aggregateUsage(allResults),
|
|
328
|
+
escalationReason: reason,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildImplTask(task: PlanTask): string {
|
|
333
|
+
const files = task.files.length > 0 ? `\nFiles: ${task.files.join(", ")}` : "";
|
|
334
|
+
return `Implement: ${task.title}\n\nDescription: ${task.description}${files}\n\nFollow TDD strictly. Write failing tests first, then implement, then refactor.`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildReviewTask(task: PlanTask, reviewType: string, changedFiles: string): string {
|
|
338
|
+
return `Review (${reviewType}) for task: ${task.title}\n\nDescription: ${task.description}\n\nChanged files: ${changedFiles}\n\nRead the actual code. Do NOT trust any self-report. End with a \`\`\`superteam-json block.`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildFixTask(task: PlanTask, reviewType: string, findings: ReviewFindings, changedFiles: string): string {
|
|
342
|
+
const findingsStr = formatFindings(findings, reviewType);
|
|
343
|
+
return `Fix ${reviewType} review findings for task: ${task.title}\n\nChanged files: ${changedFiles}\n\n${findingsStr}\n\nFix ALL mustFix items. Follow TDD — update tests if needed.`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getTrackedFiles(cwd: string): string[] {
|
|
347
|
+
try {
|
|
348
|
+
const output = execSync("git diff --name-only HEAD 2>/dev/null || git ls-files 2>/dev/null", {
|
|
349
|
+
cwd,
|
|
350
|
+
encoding: "utf-8",
|
|
351
|
+
timeout: 5000,
|
|
352
|
+
});
|
|
353
|
+
return output
|
|
354
|
+
.split("\n")
|
|
355
|
+
.map((f) => f.trim())
|
|
356
|
+
.filter(Boolean);
|
|
357
|
+
} catch {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function computeChangedFiles(before: string[], after: string[], cwd: string): string[] {
|
|
363
|
+
try {
|
|
364
|
+
const output = execSync("git diff --name-only 2>/dev/null", {
|
|
365
|
+
cwd,
|
|
366
|
+
encoding: "utf-8",
|
|
367
|
+
timeout: 5000,
|
|
368
|
+
});
|
|
369
|
+
const changed = output
|
|
370
|
+
.split("\n")
|
|
371
|
+
.map((f) => f.trim())
|
|
372
|
+
.filter(Boolean);
|
|
373
|
+
return changed.length > 0 ? changed : [];
|
|
374
|
+
} catch {
|
|
375
|
+
// Fallback: files in after but not in before
|
|
376
|
+
const beforeSet = new Set(before);
|
|
377
|
+
return after.filter((f) => !beforeSet.has(f));
|
|
378
|
+
}
|
|
379
|
+
}
|