pi-super-dev 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 +35 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/agents/adversarial-reviewer.md +64 -0
- package/agents/architecture-designer.md +43 -0
- package/agents/architecture-improver.md +46 -0
- package/agents/bdd-scenario-writer.md +37 -0
- package/agents/build-cleaner.md +44 -0
- package/agents/code-assessor.md +24 -0
- package/agents/code-reviewer.md +59 -0
- package/agents/debug-analyzer.md +54 -0
- package/agents/docs-executor.md +49 -0
- package/agents/handoff-writer.md +62 -0
- package/agents/implementer.md +47 -0
- package/agents/orchestrator.md +42 -0
- package/agents/product-designer.md +42 -0
- package/agents/prototype-runner.md +36 -0
- package/agents/qa-agent.md +76 -0
- package/agents/requirements-clarifier.md +58 -0
- package/agents/research-agent.md +33 -0
- package/agents/spec-reviewer.md +46 -0
- package/agents/spec-writer.md +32 -0
- package/agents/tdd-guide.md +51 -0
- package/agents/ui-ux-designer.md +50 -0
- package/package.json +40 -0
- package/skills/super-dev/SKILL.md +35 -0
- package/src/agents.ts +38 -0
- package/src/control.ts +85 -0
- package/src/doc-validators.ts +164 -0
- package/src/extension.ts +164 -0
- package/src/helpers.ts +263 -0
- package/src/nodes.ts +550 -0
- package/src/pi-spawn.ts +296 -0
- package/src/pipeline.ts +15 -0
- package/src/prompts.ts +120 -0
- package/src/session-agent.ts +305 -0
- package/src/setup.ts +141 -0
- package/src/stages/design.ts +33 -0
- package/src/stages/implementation.ts +80 -0
- package/src/stages/index.ts +172 -0
- package/src/stages/prototype.ts +43 -0
- package/src/stages/setup.ts +32 -0
- package/src/stages/writers.ts +105 -0
- package/src/types.ts +235 -0
- package/src/workflow.ts +181 -0
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pipeline helpers — pure functions over control JSON. Ported
|
|
3
|
+
* from the original pi-workflow `helpers/*.mjs` so agent contracts are
|
|
4
|
+
* unchanged. `runHelper(name, sources, options, context)` dispatches.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ControlObj, HelperCall, HelperResult, SetupControl } from "./types.ts";
|
|
8
|
+
import {
|
|
9
|
+
readSpecDoc,
|
|
10
|
+
specDocExists,
|
|
11
|
+
toNumber,
|
|
12
|
+
toBool,
|
|
13
|
+
isApprovedVerdict,
|
|
14
|
+
requirementsContentErrors,
|
|
15
|
+
bddContentErrors,
|
|
16
|
+
specContentErrors,
|
|
17
|
+
specReviewContentErrors,
|
|
18
|
+
} from "./doc-validators.ts";
|
|
19
|
+
|
|
20
|
+
const ok = (digest: string, value: ControlObj): HelperResult => ({ value, digest });
|
|
21
|
+
const fail = (gate: string, errors: string[]): HelperResult => ({
|
|
22
|
+
value: { pass: errors.length === 0, errors, gate },
|
|
23
|
+
digest: errors.length === 0 ? "PASS" : `FAIL: ${errors.length} error(s)`,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ─── classify-task ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const BUG_RE = /\b(bug|fix|broken|crash|error|panic|fail|regression)\b/i;
|
|
29
|
+
const REFACTOR_RE = /\b(refactor|restructure|improve|cleanup|clean up)\b/i;
|
|
30
|
+
|
|
31
|
+
function classifyTask(s: Record<string, unknown>, o?: Record<string, unknown>): HelperResult {
|
|
32
|
+
const setup = s["setup"] as { language?: string; isWebUi?: boolean } | undefined;
|
|
33
|
+
if (!setup) return ok("FAIL: missing setup source", { taskType: "feature", uiScope: "none", language: "mixed", isWebUi: false, skipStages: [] });
|
|
34
|
+
const language = setup.language ?? "mixed";
|
|
35
|
+
const isWebUi = setup.isWebUi ?? false;
|
|
36
|
+
const task = (o?.runtimeTask as string) ?? "";
|
|
37
|
+
const taskType: "bug" | "feature" | "refactor" = BUG_RE.test(task) ? "bug" : REFACTOR_RE.test(task) ? "refactor" : "feature";
|
|
38
|
+
const uiScope = isWebUi ? "ui+arch" : "none";
|
|
39
|
+
return ok(`Task: ${taskType}, UI: ${uiScope}, Lang: ${language}`, { taskType, uiScope, language, isWebUi, skipStages: [] });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── route-designer ─────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function routeDesigner(s: Record<string, unknown>): HelperResult {
|
|
45
|
+
const c = s["classify-task"] as { taskType?: string; uiScope?: string } | undefined;
|
|
46
|
+
if (!c) return ok("FAIL: missing classify-task source", { designerAgent: null, reason: "Missing upstream: classify-task" });
|
|
47
|
+
let designerAgent: string | null = null;
|
|
48
|
+
let reason = "";
|
|
49
|
+
if (c.taskType === "bug") reason = "Bug fixes do not redesign";
|
|
50
|
+
else if (c.uiScope === "ui+arch") { designerAgent = "product-designer"; reason = "Both UI and architecture changes needed"; }
|
|
51
|
+
else if (c.uiScope === "ui-only") { designerAgent = "ui-ux-designer"; reason = "UI-only changes"; }
|
|
52
|
+
else if (c.taskType === "refactor") { designerAgent = "architecture-improver"; reason = "Refactoring existing architecture"; }
|
|
53
|
+
else { designerAgent = "architecture-designer"; reason = "New feature requires architecture design"; }
|
|
54
|
+
return ok(designerAgent ? `Route to ${designerAgent}` : "Skip design (bug fix)", { designerAgent, reason });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── check-prototype-needed ──────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function checkPrototypeNeeded(s: Record<string, unknown>): HelperResult {
|
|
60
|
+
const design = s["design"] as { hasNumericConstants?: boolean; modules?: Array<{ constants?: string[] }> } | undefined;
|
|
61
|
+
if (!design) return ok("No design source — prototype not needed", { needed: false, constants: [] });
|
|
62
|
+
const needed = design.hasNumericConstants === true;
|
|
63
|
+
const constants: string[] = [];
|
|
64
|
+
if (needed && Array.isArray(design.modules)) for (const m of design.modules) if (Array.isArray(m.constants)) constants.push(...m.constants);
|
|
65
|
+
return ok(needed ? `Prototype needed: ${constants.length} constant(s)` : "Prototype not needed", { needed, constants });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── route-specialist ────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const LANG_INSTRUCTIONS: Record<string, string> = {
|
|
71
|
+
rust: "Follow Rust Edition 2024 idioms. Use thiserror for errors, tokio for async. Prefer zero-copy and ownership patterns. Run cargo clippy and cargo test.",
|
|
72
|
+
go: "Follow Go 1.24+ idioms. Use structured errors with fmt.Errorf and %w. Prefer table-driven tests. Run go vet and go test ./...",
|
|
73
|
+
frontend: "Use React 19+ patterns with TypeScript strict mode. Prefer server components where applicable. Follow existing component patterns and design tokens.",
|
|
74
|
+
backend: "Follow existing backend patterns. Use dependency injection. Write integration tests alongside unit tests. Validate error handling paths.",
|
|
75
|
+
mixed: "Respect the dominant language patterns in each file. Match surrounding code style. Test both frontend and backend changes.",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function routeSpecialist(s: Record<string, unknown>): HelperResult {
|
|
79
|
+
const c = s["classify-task"] as { language?: string } | undefined;
|
|
80
|
+
if (!c) return ok("FAIL: missing classify-task source", { specialistAgent: "implementer", languageInstructions: "", reason: "Missing upstream: classify-task" });
|
|
81
|
+
const languageInstructions = LANG_INSTRUCTIONS[c.language ?? "mixed"] ?? LANG_INSTRUCTIONS.mixed;
|
|
82
|
+
return ok(`Specialist: implementer (${c.language ?? "mixed"})`, { specialistAgent: "implementer", languageInstructions, reason: `Generic implementer with ${c.language ?? "mixed"}-specific prompt augmentation` });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── gates ──────────────────────────────────────────────────────────────────
|
|
86
|
+
// Each spec-stage gate validates the ACTUAL .md file the agent wrote (via
|
|
87
|
+
// doc-validators.ts), falling back to the agent's self-reported control JSON
|
|
88
|
+
// only when no doc can be found on disk. Content checks are authoritative —
|
|
89
|
+
// they catch false negatives where the doc is good but the control object is
|
|
90
|
+
// misshapen (the BDD gate failure). Metadata gates (build, review) coerce
|
|
91
|
+
// string↔number↔boolean so a model returning "13"/"true" doesn't trip them.
|
|
92
|
+
|
|
93
|
+
function setupSpecDir(s: Record<string, unknown>): string {
|
|
94
|
+
return (s["setup"] as SetupControl | undefined)?.specDirectory ?? "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function gateRequirements(s: Record<string, unknown>): HelperResult {
|
|
98
|
+
const req = s["write-requirements"] as ControlObj | undefined;
|
|
99
|
+
const errors: string[] = [];
|
|
100
|
+
if (!req) errors.push("Missing upstream: write-requirements");
|
|
101
|
+
else {
|
|
102
|
+
const doc = readSpecDoc(setupSpecDir(s), req, "*-requirements.md");
|
|
103
|
+
if (doc) errors.push(...requirementsContentErrors(doc.content));
|
|
104
|
+
else {
|
|
105
|
+
// No doc on disk — fall back to self-reported metadata.
|
|
106
|
+
if (!req.docPath) errors.push("No requirements doc found (no docPath, and no *-requirements.md in the spec dir)");
|
|
107
|
+
if ((toNumber(req.acCount) ?? 0) < 1) errors.push("Missing acceptance criteria");
|
|
108
|
+
if (!req.summary) errors.push("Missing summary section");
|
|
109
|
+
if (!req.featureName) errors.push("Missing feature name");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return fail("gate-requirements", errors);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function gateBdd(s: Record<string, unknown>): HelperResult {
|
|
116
|
+
const bdd = s["write-bdd"] as ControlObj | undefined;
|
|
117
|
+
const errors: string[] = [];
|
|
118
|
+
if (!bdd) errors.push("Missing upstream: write-bdd");
|
|
119
|
+
else {
|
|
120
|
+
const doc = readSpecDoc(setupSpecDir(s), bdd, "*-bdd-scenarios.md");
|
|
121
|
+
if (doc) {
|
|
122
|
+
errors.push(...bddContentErrors(doc.content));
|
|
123
|
+
} else {
|
|
124
|
+
// No doc on disk — fall back to self-reported metadata (coerced).
|
|
125
|
+
if (!bdd.docPath) errors.push("No BDD doc found (no docPath, and no *-bdd-scenarios.md in the spec dir)");
|
|
126
|
+
if ((toNumber(bdd.scenarioCount) ?? 0) < 1) errors.push("No scenarios written");
|
|
127
|
+
const score = toNumber(bdd.coverageScore);
|
|
128
|
+
const edgeOk = toBool(bdd.edgeCasesCovered) || (score !== null && score >= 0.6);
|
|
129
|
+
if (!edgeOk) errors.push("Insufficient edge case coverage (need edgeCasesCovered or coverageScore >= 0.6)");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return fail("gate-bdd", errors);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function gateSpecTrace(s: Record<string, unknown>): HelperResult {
|
|
136
|
+
const spec = s["write-spec"] as ControlObj | undefined;
|
|
137
|
+
const errors: string[] = [];
|
|
138
|
+
if (!spec) errors.push("Missing upstream: write-spec");
|
|
139
|
+
else {
|
|
140
|
+
// The implementation stage reads spec.phases from the CONTROL object, so it
|
|
141
|
+
// MUST be a usable array — validate this ALWAYS, not only on the metadata
|
|
142
|
+
// fallback path. (A good doc content but malformed phases control crashed
|
|
143
|
+
// Stage 9 with "phases.entries is not a function".)
|
|
144
|
+
if (!Array.isArray(spec.phases) || spec.phases.length === 0) errors.push("spec.phases must be a non-empty array of {name, description} objects (the implementation stage iterates it)");
|
|
145
|
+
else {
|
|
146
|
+
const unnamed = (spec.phases as Array<{ name?: string }>).filter((p) => !p?.name);
|
|
147
|
+
if (unnamed.length > 0) errors.push(`${unnamed.length} phase(s) missing a name`);
|
|
148
|
+
}
|
|
149
|
+
if ((toNumber(spec.phaseCount) ?? 0) < 1) errors.push("Phase count must be at least 1");
|
|
150
|
+
const dir = setupSpecDir(s);
|
|
151
|
+
const doc = readSpecDoc(dir, spec, "*-specification.md", ["specificationPath", "docPath"]);
|
|
152
|
+
if (doc) {
|
|
153
|
+
errors.push(...specContentErrors(doc.content));
|
|
154
|
+
if (!specDocExists(dir, "*-task-list.md")) errors.push("Task list file (*-task-list.md) missing");
|
|
155
|
+
if (!specDocExists(dir, "*-implementation-plan.md")) errors.push("Implementation plan file (*-implementation-plan.md) missing");
|
|
156
|
+
} else if (!spec.specificationPath) {
|
|
157
|
+
errors.push("No specification path returned and no *-specification.md in the spec dir");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return fail("gate-spec-trace", errors);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function gateSpecReview(s: Record<string, unknown>): HelperResult {
|
|
164
|
+
const review = s["review-spec"] as ControlObj | undefined;
|
|
165
|
+
const errors: string[] = [];
|
|
166
|
+
if (!review) errors.push("Missing upstream: review-spec");
|
|
167
|
+
else {
|
|
168
|
+
const doc = readSpecDoc(setupSpecDir(s), review, "*-spec-review*.md");
|
|
169
|
+
if (doc) errors.push(...specReviewContentErrors(doc.content));
|
|
170
|
+
if (!isApprovedVerdict(review.verdict)) errors.push(`Verdict is "${review.verdict ?? ""}" — changes requested`);
|
|
171
|
+
}
|
|
172
|
+
return fail("gate-spec-review", errors);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function gateBuild(s: Record<string, unknown>): HelperResult {
|
|
176
|
+
const qa = s["qa-check"] as ControlObj | undefined;
|
|
177
|
+
const errors: string[] = [];
|
|
178
|
+
if (!qa) errors.push("Missing upstream: qa-check");
|
|
179
|
+
else {
|
|
180
|
+
if (!toBool(qa.buildSuccess)) errors.push("Build failed");
|
|
181
|
+
if (!toBool(qa.allTestsPass)) errors.push("Tests failing");
|
|
182
|
+
}
|
|
183
|
+
return fail("gate-build", errors);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function gateReview(s: Record<string, unknown>): HelperResult {
|
|
187
|
+
const merged = s["merge-verdicts"] as ControlObj | undefined;
|
|
188
|
+
const errors: string[] = [];
|
|
189
|
+
if (!merged) errors.push("Missing upstream: merge-verdicts");
|
|
190
|
+
else if (!isApprovedVerdict(merged.verdict)) errors.push(`Verdict is "${merged.verdict ?? ""}" — changes requested`);
|
|
191
|
+
return fail("gate-review", errors);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── merge-review-verdicts ──────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
const VERDICT_RANK: Record<string, number> = { Approved: 0, "Approved with Comments": 1, "Changes Requested": 2 };
|
|
197
|
+
|
|
198
|
+
function mergeReviewVerdicts(s: Record<string, unknown>): HelperResult {
|
|
199
|
+
const codeReview = s["code-review"] as ControlObj | undefined;
|
|
200
|
+
const adversarial = s["adversarial-review"] as ControlObj | undefined;
|
|
201
|
+
if (!codeReview && !adversarial) return ok("FAIL: missing both review sources", { verdict: "Changes Requested", findings: [], dimensionsCovered: [] });
|
|
202
|
+
const codeVerdict = (codeReview?.verdict as string) ?? "Approved";
|
|
203
|
+
const advVerdict = (adversarial?.verdict as string) ?? "Approved";
|
|
204
|
+
const verdict = (VERDICT_RANK[codeVerdict] ?? 0) >= (VERDICT_RANK[advVerdict] ?? 0) ? codeVerdict : advVerdict;
|
|
205
|
+
const findings = [...((codeReview?.findings as unknown[]) ?? []), ...((adversarial?.findings as unknown[]) ?? [])];
|
|
206
|
+
const dims = [...new Set([...((codeReview?.dimensionsCovered as unknown[]) ?? []), ...((adversarial?.dimensionsCovered as unknown[]) ?? [])] as string[])];
|
|
207
|
+
return ok(`Merged verdict: ${verdict} (${findings.length} finding(s))`, { verdict, findings, dimensionsCovered: dims });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── cleanup ────────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
const BUILD_DIRS = new Set(["node_modules", "target", "dist", "build", "__pycache__", ".next", ".nuxt", ".output", "coverage", ".turbo"]);
|
|
213
|
+
const SENSITIVE_RE = [/\.env$/, /\.env\.local$/, /\.env\.production$/, /\.pem$/, /\.key$/, /id_rsa/, /id_ed25519/, /\.p12$/, /credentials\.json$/, /service[-_]account.*\.json$/];
|
|
214
|
+
const LANG_MARKERS: Record<string, string[]> = {
|
|
215
|
+
rust: ["Cargo.toml", "Cargo.lock"], go: ["go.mod", "go.sum"], frontend: ["package.json", "tsconfig.json"], python: ["pyproject.toml", "setup.py", "requirements.txt"],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
async function cleanup(_s: Record<string, unknown>, context?: Record<string, unknown>): Promise<HelperResult> {
|
|
219
|
+
const cwd = context?.cwd as string | undefined;
|
|
220
|
+
if (!cwd) return ok("FAIL: no cwd in context", { languagesDetected: [], directoriesRemoved: [], sensitiveDataFindings: [], blocked: false, summary: "Could not scan — no working directory provided" });
|
|
221
|
+
const { readdir, stat } = await import("node:fs/promises");
|
|
222
|
+
const { join } = await import("node:path");
|
|
223
|
+
const languagesDetected: string[] = [];
|
|
224
|
+
for (const [lang, markers] of Object.entries(LANG_MARKERS)) {
|
|
225
|
+
for (const marker of markers) {
|
|
226
|
+
try { await stat(join(cwd, marker)); if (!languagesDetected.includes(lang)) languagesDetected.push(lang); break; } catch { /* absent */ }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const directoriesRemoved: string[] = [];
|
|
230
|
+
try { for (const e of await readdir(cwd, { withFileTypes: true })) if (e.isDirectory() && BUILD_DIRS.has(e.name)) directoriesRemoved.push(e.name); } catch { /* unreadable */ }
|
|
231
|
+
const sensitiveDataFindings: string[] = [];
|
|
232
|
+
try { for (const e of await readdir(cwd)) for (const re of SENSITIVE_RE) if (re.test(e)) { sensitiveDataFindings.push(`Sensitive file detected: ${e}`); break; } } catch { /* unreadable */ }
|
|
233
|
+
const blocked = sensitiveDataFindings.length > 0;
|
|
234
|
+
return ok(blocked ? `BLOCKED: ${sensitiveDataFindings.length} sensitive finding(s)` : `Clean: ${languagesDetected.length} lang(s), ${directoriesRemoved.length} build dir(s)`, {
|
|
235
|
+
languagesDetected, directoriesRemoved, sensitiveDataFindings, blocked,
|
|
236
|
+
summary: blocked ? `Merge blocked: found ${sensitiveDataFindings.length} sensitive data issue(s)` : `Worktree clean. Languages: ${languagesDetected.join(", ") || "none detected"}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── dispatcher ─────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
const SYNC: Record<string, (s: Record<string, unknown>, o?: Record<string, unknown>) => HelperResult> = {
|
|
243
|
+
"classify-task": classifyTask,
|
|
244
|
+
"route-designer": routeDesigner,
|
|
245
|
+
"check-prototype-needed": checkPrototypeNeeded,
|
|
246
|
+
"route-specialist": routeSpecialist,
|
|
247
|
+
"gate-requirements": gateRequirements,
|
|
248
|
+
"gate-bdd": gateBdd,
|
|
249
|
+
"gate-spec-trace": gateSpecTrace,
|
|
250
|
+
"gate-spec-review": gateSpecReview,
|
|
251
|
+
"gate-build": gateBuild,
|
|
252
|
+
"gate-review": gateReview,
|
|
253
|
+
"merge-review-verdicts": mergeReviewVerdicts,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export async function runHelper(call: HelperCall): Promise<HelperResult> {
|
|
257
|
+
if (call.name === "cleanup") return cleanup(call.sources, call.context);
|
|
258
|
+
const fn = SYNC[call.name];
|
|
259
|
+
if (!fn) return ok(`FAIL: unknown helper "${call.name}"`, {});
|
|
260
|
+
return fn(call.sources, call.options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export const HELPER_NAMES = [...Object.keys(SYNC), "cleanup"];
|