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.
Files changed (45) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/agents/adversarial-reviewer.md +64 -0
  5. package/agents/architecture-designer.md +43 -0
  6. package/agents/architecture-improver.md +46 -0
  7. package/agents/bdd-scenario-writer.md +37 -0
  8. package/agents/build-cleaner.md +44 -0
  9. package/agents/code-assessor.md +24 -0
  10. package/agents/code-reviewer.md +59 -0
  11. package/agents/debug-analyzer.md +54 -0
  12. package/agents/docs-executor.md +49 -0
  13. package/agents/handoff-writer.md +62 -0
  14. package/agents/implementer.md +47 -0
  15. package/agents/orchestrator.md +42 -0
  16. package/agents/product-designer.md +42 -0
  17. package/agents/prototype-runner.md +36 -0
  18. package/agents/qa-agent.md +76 -0
  19. package/agents/requirements-clarifier.md +58 -0
  20. package/agents/research-agent.md +33 -0
  21. package/agents/spec-reviewer.md +46 -0
  22. package/agents/spec-writer.md +32 -0
  23. package/agents/tdd-guide.md +51 -0
  24. package/agents/ui-ux-designer.md +50 -0
  25. package/package.json +40 -0
  26. package/skills/super-dev/SKILL.md +35 -0
  27. package/src/agents.ts +38 -0
  28. package/src/control.ts +85 -0
  29. package/src/doc-validators.ts +164 -0
  30. package/src/extension.ts +164 -0
  31. package/src/helpers.ts +263 -0
  32. package/src/nodes.ts +550 -0
  33. package/src/pi-spawn.ts +296 -0
  34. package/src/pipeline.ts +15 -0
  35. package/src/prompts.ts +120 -0
  36. package/src/session-agent.ts +305 -0
  37. package/src/setup.ts +141 -0
  38. package/src/stages/design.ts +33 -0
  39. package/src/stages/implementation.ts +80 -0
  40. package/src/stages/index.ts +172 -0
  41. package/src/stages/prototype.ts +43 -0
  42. package/src/stages/setup.ts +32 -0
  43. package/src/stages/writers.ts +105 -0
  44. package/src/types.ts +235 -0
  45. package/src/workflow.ts +181 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Spawns `pi` child processes to run specialist agents — the single primitive
3
+ * that replaces pi-workflow's agent engine. Verified invocation:
4
+ *
5
+ * pi --mode json -p --no-session --no-skills [--no-extensions] \
6
+ * --tools read,bash,edit,write,ffgrep,fffind \
7
+ * [--model <provider/id>] --system-prompt <temp-file> "Task: <prompt>"
8
+ *
9
+ * stdout is newline-delimited JSON; the final assistant text is in the last
10
+ * `{"type":"message_end","message":{"role":"assistant",...}}` event.
11
+ */
12
+
13
+ import { spawn } from "node:child_process";
14
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { loadAgentPrompt } from "./agents.ts";
18
+ import { extractControl } from "./control.ts";
19
+ import type { AgentProgress, SpawnResult } from "./types.ts";
20
+
21
+ const BASE_TOOLS = "read,bash,edit,write,ffgrep,fffind";
22
+
23
+ /** Agents that drive a browser for UI testing. They receive the `browser_execute`
24
+ * tool and load extensions (so pi-browser-cdp-extension is available). The
25
+ * `--tools` allowlist still keeps every other extension tool (e.g. `subagent`)
26
+ * disabled, so this stays isolated. Browser connection uses AUTO-DISCOVERY —
27
+ * `await session.connect()` with no args finds any Chrome started with
28
+ * `--remote-debugging-port`; see agents/qa-agent.md. */
29
+ const BROWSER_AGENTS = new Set(["qa-agent"]);
30
+
31
+ export function isBrowserAgent(agent: string): boolean {
32
+ return BROWSER_AGENTS.has(agent);
33
+ }
34
+
35
+ export function toolsForAgent(agent: string): string {
36
+ return BROWSER_AGENTS.has(agent) ? `${BASE_TOOLS},browser_execute` : BASE_TOOLS;
37
+ }
38
+
39
+ /** Per-spawn wall-clock cap. Generous: capable agents legitimately take 1–2 min. */
40
+ const DEFAULT_SPAWN_TIMEOUT_MS = 480_000;
41
+
42
+ export interface SpawnAgentOptions {
43
+ agent: string;
44
+ prompt: string;
45
+ cwd: string;
46
+ model?: string;
47
+ signal?: AbortSignal;
48
+ id?: string;
49
+ timeoutMs?: number;
50
+ /** Ignored by the subprocess backend (it uses <control> text, not a schema).
51
+ * Accepted so the same `common` options object can feed both backends. */
52
+ controlKeys?: string[];
53
+ /** Live progress from the spawned agent (tool calls + streaming text). */
54
+ onProgress?: AgentProgress;
55
+ }
56
+
57
+ function resolvePiBinary(): { command: string; args: string[] } {
58
+ const argv1 = process.argv[1] ?? "";
59
+ if (argv1 && /\.(?:mjs|cjs|js)$/i.test(argv1)) {
60
+ return { command: process.execPath, args: [argv1] };
61
+ }
62
+ return { command: "pi", args: [] };
63
+ }
64
+
65
+ export async function spawnAgent(opts: SpawnAgentOptions): Promise<SpawnResult> {
66
+ const systemPrompt = loadAgentPrompt(opts.agent);
67
+ const tempDir = mkdtempSync(join(tmpdir(), "super-dev-agent-"));
68
+ const promptPath = join(tempDir, "agent.md");
69
+ writeFileSync(promptPath, systemPrompt, { mode: 0o600 });
70
+
71
+ const args = buildSpawnArgs(opts, promptPath);
72
+ const result = await runPi(args, opts.cwd, opts.signal, opts.id ?? opts.agent, opts.timeoutMs ?? DEFAULT_SPAWN_TIMEOUT_MS, opts.onProgress);
73
+ rmSync(tempDir, { recursive: true, force: true });
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Build the full argv vector for a specialist spawn, INCLUDING the executable
79
+ * as element 0. (Extracted so the command resolution is unit-testable — a
80
+ * previous version dropped `command` and tried to exec "--mode", causing
81
+ * `spawn --mode ENOENT` on every single agent spawn.)
82
+ *
83
+ * Browser-capable agents (see BROWSER_AGENTS) omit `--no-extensions` so the
84
+ * pi-browser-cdp-extension loads, and add `browser_execute` to the tool set.
85
+ * The `--tools` allowlist still restricts active tools to the declared set.
86
+ */
87
+ export function buildSpawnArgs(opts: SpawnAgentOptions, promptPath: string): string[] {
88
+ const { command, args: prefix } = resolvePiBinary();
89
+ const browser = isBrowserAgent(opts.agent);
90
+ const args = [
91
+ command, // ← the executable ("pi" on PATH, or `node` re-invoking the host entry)
92
+ ...prefix,
93
+ "--mode", "json", "-p", "--no-session", "--no-skills",
94
+ ];
95
+ // Browser agents need pi-browser-cdp-extension loaded, so they do NOT pass
96
+ // --no-extensions. The --tools allowlist below still restricts active tools
97
+ // to the declared set (so loading extensions doesn't enable e.g. `subagent`).
98
+ if (!browser) args.push("--no-extensions");
99
+ args.push("--tools", toolsForAgent(opts.agent));
100
+ args.push("--system-prompt", promptPath);
101
+ if (opts.model) args.push("--model", opts.model);
102
+ args.push(`Task: ${opts.prompt}`);
103
+ return args;
104
+ }
105
+
106
+ function runPi(args: string[], cwd: string, signal: AbortSignal | undefined, label: string, timeoutMs: number, onProgress?: AgentProgress): Promise<SpawnResult> {
107
+ return new Promise((resolve, reject) => {
108
+ const child = spawn(args[0], args.slice(1), {
109
+ cwd,
110
+ stdio: ["ignore", "pipe", "pipe"],
111
+ env: { ...process.env },
112
+ windowsHide: true,
113
+ });
114
+ // Bounded capture ONLY: the spawned agent's stdout is a stream of NDJSON
115
+ // deltas where each message_update re-emits the FULL accumulated partial —
116
+ // gigabytes for a verbose/long agent (the design stage crashed pi with
117
+ // RangeError "Invalid string length" at >512MB). Never buffer the whole
118
+ // stdout; parse line-by-line and keep only the last assistant text.
119
+ let lineBuf = "";
120
+ let lastAssistantText = "";
121
+ let lastModel: string | undefined;
122
+ let stderrBuf = "";
123
+ let aborted = false;
124
+ let timedOut = false;
125
+ let turns = 0;
126
+ let currentText = ""; // live streaming text of the current agent text block
127
+ const STDERR_CAP = 16 * 1024;
128
+ const LINE_CAP = 16 * 1024 * 1024;
129
+ const cleanup = () => {
130
+ signal?.removeEventListener("abort", onAbort);
131
+ clearTimeout(timer);
132
+ };
133
+ const onAbort = () => {
134
+ aborted = true;
135
+ try { child.kill("SIGTERM"); } catch { /* ignore */ }
136
+ };
137
+ signal?.addEventListener("abort", onAbort, { once: true });
138
+ const timer = setTimeout(() => {
139
+ timedOut = true;
140
+ try { child.kill("SIGTERM"); } catch { /* ignore */ }
141
+ }, timeoutMs);
142
+
143
+ child.stdout.on("data", (c: Buffer) => {
144
+ lineBuf += c.toString("utf8");
145
+ let nl: number;
146
+ while ((nl = lineBuf.indexOf("\n")) >= 0) {
147
+ const raw = lineBuf.slice(0, nl);
148
+ lineBuf = lineBuf.slice(nl + 1);
149
+ const trimmed = raw.trim();
150
+ if (!trimmed) continue;
151
+ let ev: PiJsonEvent;
152
+ try { ev = JSON.parse(trimmed) as PiJsonEvent; } catch { continue; }
153
+ // capture the final assistant text (for <control> extraction)
154
+ const a = assistantFromMessageEnd(ev);
155
+ if (a) {
156
+ if (a.text) { lastAssistantText = a.text; if (a.model) lastModel = a.model; }
157
+ // a finished message finalizes any in-progress live text
158
+ if (onProgress && currentText.trim()) { onProgress.event(stripControl(currentText).trim()); currentText = ""; }
159
+ continue;
160
+ }
161
+ if (!onProgress) continue;
162
+ const se = renderEvent(ev, () => ++turns);
163
+ if (!se) continue;
164
+ if (se.kind === "text") {
165
+ // live typing: update the mutable live line
166
+ currentText = se.text;
167
+ onProgress.text(stripControl(currentText));
168
+ } else {
169
+ // a permanent event finalizes any in-progress text first
170
+ if (currentText.trim()) { onProgress.event(stripControl(currentText).trim()); currentText = ""; }
171
+ if (se.kind === "tool") onProgress.event(`→ ${se.summary}`);
172
+ else if (se.kind === "turn" && se.n > 1) onProgress.event(`turn ${se.n}`);
173
+ }
174
+ }
175
+ if (lineBuf.length > LINE_CAP) lineBuf = ""; // stay bounded on a runaway line
176
+ });
177
+ child.stderr.on("data", (c: Buffer) => {
178
+ stderrBuf += c.toString("utf8");
179
+ if (stderrBuf.length > STDERR_CAP) stderrBuf = stderrBuf.slice(stderrBuf.length - STDERR_CAP);
180
+ });
181
+ child.on("error", (err) => {
182
+ cleanup();
183
+ reject(new Error(`super-dev [${label}]: failed to spawn pi: ${err.message}`));
184
+ });
185
+ child.on("close", (code) => {
186
+ cleanup();
187
+ if (aborted) { resolve({ text: "", control: null, error: "aborted" }); return; }
188
+ // lastAssistantText already holds the last non-empty assistant text
189
+ // (resilient to a trailing tool-call turn or a mid-stream kill).
190
+ if (lastAssistantText) {
191
+ resolve({ text: lastAssistantText, control: extractControl(lastAssistantText), model: lastModel, error: timedOut ? `timed out after ${timeoutMs}ms (used partial output)` : undefined });
192
+ return;
193
+ }
194
+ const tail = stderrBuf.trim().split("\n").slice(-3).join(" | ");
195
+ const reason = timedOut ? `timed out after ${Math.round(timeoutMs / 1000)}s` : `produced no output (exit ${code})`;
196
+ reject(new Error(`super-dev [${label}]: agent ${reason}.${tail ? ` stderr: ${tail}` : ""}`));
197
+ });
198
+ });
199
+ }
200
+
201
+ interface PiJsonEvent {
202
+ type?: string;
203
+ toolName?: string;
204
+ args?: Record<string, unknown>;
205
+ message?: { role?: string; model?: string; content?: Array<{ type: string; text?: string }> };
206
+ }
207
+
208
+ /** If an event is an assistant message_end, return its text + model (shared by
209
+ * the streaming capture and the batch extractFinalAssistant). */
210
+ function assistantFromMessageEnd(ev: PiJsonEvent): { text: string; model?: string } | null {
211
+ if (ev.type !== "message_end" || ev.message?.role !== "assistant") return null;
212
+ const text = (ev.message.content ?? [])
213
+ .filter((p) => p.type === "text" && typeof p.text === "string")
214
+ .map((p) => p.text as string)
215
+ .join("");
216
+ return { text, model: ev.message.model };
217
+ }
218
+
219
+ /** Compact one-line summary of a tool call, for live progress.
220
+ * Paths/commands are shown IN FULL (no truncation, no abbreviation) — the
221
+ * TUI wraps long lines, same as it does for read/write. */
222
+ export function summarizeToolCall(name: string, args: Record<string, unknown> | undefined): string {
223
+ const a = args ?? {};
224
+ switch (name) {
225
+ case "write":
226
+ case "edit":
227
+ case "read":
228
+ return `${name} ${a.path ?? a.file_path ?? ""}`;
229
+ case "bash":
230
+ return `$ ${String(a.command ?? "").split("\n")[0]}`;
231
+ case "ffgrep":
232
+ case "fffind":
233
+ return `${name} "${a.pattern ?? ""}"`;
234
+ default:
235
+ return name;
236
+ }
237
+ }
238
+
239
+ /** Shorten a path/string for display: cwd => ".", $HOME => "~". Keeps live
240
+ * progress readable instead of being truncated mid-path by the TUI. */
241
+ export function abbreviatePath(p: string, cwd?: string): string {
242
+ if (!p) return p;
243
+ let out = p;
244
+ if (cwd && cwd.length > 1 && out.includes(cwd)) out = out.split(cwd).join(".");
245
+ const home = process.env.HOME;
246
+ if (home && out.startsWith(home)) out = "~" + out.slice(home.length);
247
+ return out;
248
+ }
249
+
250
+ /** Parse one streamed NDJSON line: surface live progress AND capture the
251
+ * assistant text. Returns {text,model} if the line is an assistant message_end. */
252
+ type StreamEvent =
253
+ | { kind: "text"; text: string }
254
+ | { kind: "tool"; summary: string }
255
+ | { kind: "turn"; n: number };
256
+
257
+ /** Strip the machine <control> block from displayed text. */
258
+ function stripControl(s: string): string {
259
+ return s.replace(/<control>[\s\S]*?<\/control>/gi, "");
260
+ }
261
+
262
+ /** Extract a renderable event from a parsed NDJSON line (pure).
263
+ * pi streams assistant text inside `message_update` events whose `message.content`
264
+ * holds the full accumulated text so far. */
265
+ export function renderEvent(ev: PiJsonEvent, nextTurn: () => number): StreamEvent | null {
266
+ switch (ev.type) {
267
+ case "message_update": {
268
+ const text = (ev.message?.content ?? []).filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
269
+ return text ? { kind: "text", text } : null;
270
+ }
271
+ case "tool_execution_start":
272
+ return ev.toolName ? { kind: "tool", summary: summarizeToolCall(ev.toolName, ev.args) } : null;
273
+ case "turn_start":
274
+ return { kind: "turn", n: nextTurn() };
275
+ default:
276
+ return null;
277
+ }
278
+ }
279
+
280
+ export function extractFinalAssistant(stdout: string): { text: string; model?: string } {
281
+ let text = "";
282
+ let model: string | undefined;
283
+ for (const line of stdout.split("\n")) {
284
+ const trimmed = line.trim();
285
+ if (!trimmed) continue;
286
+ let event: PiJsonEvent;
287
+ try { event = JSON.parse(trimmed) as PiJsonEvent; } catch { continue; }
288
+ // Keep the LAST NON-EMPTY assistant text — never overwrite with empty,
289
+ // so a trailing tool-call-only turn doesn't discard the control block
290
+ // emitted in an earlier turn.
291
+ const r = assistantFromMessageEnd(event);
292
+ if (r && r.text) { text = r.text; if (r.model) model = r.model; }
293
+ }
294
+ return { text, model };
295
+ }
296
+
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Thin public entry: run the super-dev workflow for a task.
3
+ */
4
+
5
+ import { runWorkflow } from "./workflow.ts";
6
+ import { SUPER_DEV_WORKFLOW } from "./stages/index.ts";
7
+ import type { RunOptions, RunSummary } from "./types.ts";
8
+
9
+ export async function runPipelineTask(task: string, options: RunOptions = {}): Promise<RunSummary> {
10
+ return runWorkflow(SUPER_DEV_WORKFLOW, task, options);
11
+ }
12
+
13
+ export { SUPER_DEV_WORKFLOW } from "./stages/index.ts";
14
+ export { runWorkflow } from "./workflow.ts";
15
+ export type { RunSummary, RunOptions, Workflow, Node, NodeResult, PipelineState } from "./types.ts";
package/src/prompts.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Prompt builders for each pipeline stage. Ported from the original controller
3
+ * so agent `<control>` JSON contracts are unchanged.
4
+ *
5
+ * Doc NUMBERING is COMPUTED, never hardcoded: a stage's number = (number of
6
+ * numbered docs already in the spec dir) + 1. So the sequence is dense and
7
+ * follows actual execution order — a skipped stage (debug for a feature,
8
+ * prototype when there are no numeric constants) writes no file and consumes
9
+ * no number, so code-assessment lands on 04 when debug is skipped and 05 when
10
+ * debug runs. The current stage's own slug is excluded from the count so gate
11
+ * retries don't inflate it; spec's three docs take base, base+1, base+2.
12
+ */
13
+
14
+ import { readdirSync } from "node:fs";
15
+ import type { SetupControl, Classification, ControlObj } from "./types.ts";
16
+
17
+ type R = ControlObj | null | undefined;
18
+
19
+ /** Next doc number = count of existing `NN-*` files in the spec dir (excluding
20
+ * any whose name ends in `-<slug>.md` for the given slugs) + 1. */
21
+ function nextDocNumber(specDir: string, excludeSlugs: string[] = []): number {
22
+ let count = 0;
23
+ try {
24
+ for (const entry of readdirSync(specDir)) {
25
+ if (!/^\d{2}-.+/.test(entry)) continue;
26
+ if (excludeSlugs.some((sg) => entry.endsWith(`-${sg}.md`))) continue;
27
+ count++;
28
+ }
29
+ } catch { /* dir not readable yet — treat as empty */ }
30
+ return count + 1;
31
+ }
32
+
33
+ const pad = (n: number) => String(n).padStart(2, "0");
34
+
35
+ /** A single stage's doc path: next free number + the slug. */
36
+ function specDoc(s: SetupControl, slug: string): string {
37
+ return `${s.specDirectory}${pad(nextDocNumber(s.specDirectory, [slug]))}-${slug}.md`;
38
+ }
39
+
40
+ /** A stage that writes several docs at once (the spec stage): they take
41
+ * consecutive numbers base, base+1, … in the given slug order. */
42
+ function specDocRange(s: SetupControl, slugs: string[]): string[] {
43
+ const base = nextDocNumber(s.specDirectory, slugs);
44
+ return slugs.map((slug, i) => `${s.specDirectory}${pad(base + i)}-${slug}.md`);
45
+ }
46
+
47
+ function ctxBlock(setup: SetupControl, c: Classification | null): string {
48
+ return ["## Context", `- Worktree: ${setup.worktreePath}`, `- Spec Directory: ${setup.specDirectory}`, `- Language: ${c?.language ?? setup.language}`, `- Task Type: ${c?.taskType ?? "unknown"}`, `- UI Scope: ${c?.uiScope ?? "none"}`, `- Default Branch: ${setup.defaultBranch ?? "main"}`].join("\n");
49
+ }
50
+
51
+ export function buildRequirementsPrompt(s: SetupControl, c: Classification | null, task: string): string {
52
+ return [ctxBlock(s, c), "", "## Task", task, "", "## Instructions", "Produce an implementation-ready requirements document.", `Write the document to: ${specDoc(s, "requirements")}`, "Include: feature name, acceptance criteria (numbered AC-XX), open questions, and a summary.", "", "Output <control> JSON with: docPath, featureName, acCount, openQuestions, summary."].join("\n");
53
+ }
54
+ export function buildBddPrompt(s: SetupControl, c: Classification | null, task: string, requirements: R): string {
55
+ return [ctxBlock(s, c), "", "## Upstream Artifacts", `- Requirements: ${(requirements?.docPath as string) ?? "N/A"}`, "", "## Task", task, "", "## Instructions", "Write BDD behavior scenarios in Gherkin-like markdown from the requirements acceptance criteria.", `Write to: ${specDoc(s, "bdd-scenarios")}`, "Cover happy paths, edge cases, and error scenarios.", "", "Output <control> JSON with: docPath, scenarioCount, edgeCasesCovered, coverageScore, summary."].join("\n");
56
+ }
57
+ export function buildResearchPrompt(s: SetupControl, c: Classification | null, task: string, requirements: R, bdd: R, prev: R): string {
58
+ const parts = [ctxBlock(s, c), "", "## Upstream Artifacts", `- Requirements: ${(requirements?.docPath as string) ?? "N/A"}`, `- BDD Scenarios: ${(bdd?.docPath as string) ?? "N/A"}`];
59
+ if (prev?.docPath) { parts.push(`- Previous Research: ${prev.docPath as string}`); const oi = prev.openIssues as string[] | undefined; if (Array.isArray(oi) && oi.length) parts.push(`- Open Issues to resolve: ${oi.join(", ")}`); }
60
+ parts.push("", "## Task", task, "", "## Instructions", "Research best practices, documentation, and patterns relevant to this task.", `Write to: ${specDoc(s, "research-report")}`, "Identify options, tradeoffs, and open issues. Resolve any previously open issues.", "", "Output <control> JSON with: docPath, options (array), openIssues (array), iteration, summary.");
61
+ return parts.join("\n");
62
+ }
63
+ export function buildDebugPrompt(s: SetupControl, c: Classification | null, task: string, requirements: R, research: R): string {
64
+ return [ctxBlock(s, c), "", "## Upstream Artifacts", `- Requirements: ${(requirements?.docPath as string) ?? "N/A"}`, `- Research: ${(research?.docPath as string) ?? "N/A"}`, "", "## Task", task, "", "## Instructions", "Perform systematic root-cause debugging with evidence collection.", `Write to: ${specDoc(s, "debug-analysis")}`, "Include: hypotheses, reproduction steps, root cause, and recommended fix.", "", "Output <control> JSON with: docPath, hypotheses (array), rootCause, reproductionSteps, summary."].join("\n");
65
+ }
66
+ export function buildAssessmentPrompt(s: SetupControl, c: Classification | null, task: string, research: R, debug: R): string {
67
+ const parts = [ctxBlock(s, c), "", "## Upstream Artifacts", `- Research: ${(research?.docPath as string) ?? "N/A"}`];
68
+ if (debug?.docPath) parts.push(`- Debug Analysis: ${debug.docPath as string}`);
69
+ parts.push("", "## Task", task, "", "## Instructions", "Assess the existing codebase: architecture patterns, coding standards, dependencies, and framework conventions.", `Write to: ${specDoc(s, "code-assessment")}`, "Identify patterns to follow, anti-patterns to avoid, and relevant files.", "", "Output <control> JSON with: docPath, patterns (array of objects), filesAssessed, recommendations, summary.");
70
+ return parts.join("\n");
71
+ }
72
+ export function buildDesignPrompt(s: SetupControl, c: Classification | null, task: string, requirements: R, research: R, assessment: R, designerAgent: string): string {
73
+ return [ctxBlock(s, c), "", "## Upstream Artifacts", `- Requirements: ${(requirements?.docPath as string) ?? "N/A"}`, `- Research: ${(research?.docPath as string) ?? "N/A"}`, `- Code Assessment: ${(assessment?.docPath as string) ?? "N/A"}`, "", "## Task", task, "", "## Instructions", `You are the ${designerAgent}. Design the architecture/UI for this feature.`, `Write to: ${specDoc(s, "design")}`, "Include: module decomposition, interfaces, data flow, and any numeric constants that need validation.", "", "Output <control> JSON with: designer, docs (array of paths), modules (array of objects), hasNumericConstants, summary."].join("\n");
74
+ }
75
+ export function buildPrototypePrompt(s: SetupControl, c: Classification | null, task: string, design: R, constants: string[], round: number): string {
76
+ return [ctxBlock(s, c), "", "## Design", `- Design doc: ${(design?.docs as string[] | undefined)?.[0] ?? "N/A"}`, `- Constants to validate: ${(constants ?? []).join(", ")}`, "", "## Task", task, "", "## Instructions", `Prototype round ${round}: Empirically validate the numeric design constants.`, "Build a minimal prototype, measure against representative input, and report pass/fail.", `Write the report to: ${specDoc(s, "prototype-report")}`, "", "Output <control> JSON with: docPath, verdict ('pass' or 'fail'), measurements (array), adjustments (array), summary."].join("\n");
77
+ }
78
+ export function buildSpecPrompt(s: SetupControl, c: Classification | null, task: string, requirements: R, bdd: R, research: R, assessment: R, design: R): string {
79
+ const parts = [ctxBlock(s, c), "", "## Upstream Artifacts", `- Requirements: ${(requirements?.docPath as string) ?? "N/A"}`, `- BDD Scenarios: ${(bdd?.docPath as string) ?? "N/A"}`, `- Research: ${(research?.docPath as string) ?? "N/A"}`, `- Code Assessment: ${(assessment?.docPath as string) ?? "N/A"}`];
80
+ const docs = design?.docs as string[] | undefined;
81
+ if (Array.isArray(docs) && docs.length) parts.push(`- Design: ${docs.join(", ")}`);
82
+ const [specification, plan, tasks] = specDocRange(s, ["specification", "implementation-plan", "task-list"]);
83
+ parts.push("", "## Task", task, "", "## Instructions", "Write the technical specification, implementation plan, and task list.", `Write specification to: ${specification}`, `Write plan to: ${plan}`, `Write task list to: ${tasks}`, "Break implementation into phases. Each phase must be independently testable.", "", "Output <control> JSON with: specificationPath, planPath, tasksPath, phaseCount, phases (array with name/description per phase), summary.");
84
+ return parts.join("\n");
85
+ }
86
+ export function buildSpecReviewPrompt(s: SetupControl, c: Classification | null, specControl: R): string {
87
+ return [ctxBlock(s, c), "", "## Specification to Review", `- Specification: ${(specControl?.specificationPath as string) ?? "N/A"}`, `- Plan: ${(specControl?.planPath as string) ?? "N/A"}`, `- Tasks: ${(specControl?.tasksPath as string) ?? "N/A"}`, `- Phases: ${(specControl?.phaseCount as number) ?? 0}`, "", "## Instructions", "Review the specification across 8 quality dimensions: completeness, correctness, consistency, testability, feasibility, security, performance, and maintainability.", "Score each dimension 1-5. Produce a verdict.", `Write the review to: ${specDoc(s, "spec-review")}`, "", "Output <control> JSON with: docPath, verdict ('Approved'|'Approved with Comments'|'Changes Requested'), findings (array), dimensionsScored (array), summary."].join("\n");
88
+ }
89
+ export function buildTddPrompt(s: SetupControl, c: Classification | null, phase: { name: string; description?: string }, specControl: R): string {
90
+ return [ctxBlock(s, c), "", "## Implementation Phase", `- Phase: ${phase.name}`, `- Description: ${phase.description ?? ""}`, `- Specification: ${(specControl?.specificationPath as string) ?? "N/A"}`, "", "## Instructions", "Write failing tests FIRST for this implementation phase.", "Tests should cover the acceptance criteria and edge cases.", "Run the tests to confirm they fail (red phase of TDD).", "", "Output <control> JSON with: testsWritten (number), testFiles (array of paths), allFailing (boolean), summary."].join("\n");
91
+ }
92
+ export function buildImplementPrompt(s: SetupControl, c: Classification | null, phase: { name: string; description?: string }, specialist: R, specControl: R): string {
93
+ const li = (specialist?.languageInstructions as string) ?? "";
94
+ return [ctxBlock(s, c), "", "## Implementation Phase", `- Phase: ${phase.name}`, `- Description: ${phase.description ?? ""}`, `- Specification: ${(specControl?.specificationPath as string) ?? "N/A"}`, "", li ? `## Language-Specific Instructions\n${li}\n` : "", "## Instructions", "Implement the code to make the failing tests pass (green phase of TDD).", "Follow existing patterns from the code assessment. Keep changes minimal and focused.", "", "Output <control> JSON with: filesModified (array), testsPassCount (number), summary."].join("\n");
95
+ }
96
+ export function buildQaPrompt(s: SetupControl, c: Classification | null, phase: { name: string }): string {
97
+ return [ctxBlock(s, c), "", "## Implementation Phase", `- Phase: ${phase.name}`, "", "## Instructions", "Run the full test suite and verify build succeeds.", "Check coverage meets threshold. Report any regressions.", "", "Output <control> JSON with: allTestsPass (boolean), buildSuccess (boolean), coveragePercent (number), regressions (array), summary."].join("\n");
98
+ }
99
+ export function buildImplementationSummaryPrompt(s: SetupControl, c: Classification | null, impl: R): string {
100
+ return [ctxBlock(s, c), "", "## Implementation Result", `- Phases Completed: ${(impl?.phasesCompleted as number) ?? 0}/${(impl?.totalPhases as number) ?? 0}`, `- All Green: ${(impl?.allGreen as boolean) ?? false}`, `- Files Modified: ${((impl?.filesModified as string[]) ?? []).join(", ") || "none"}`, "", "## Instructions", "Write a concise implementation summary: what was built per phase, files changed, test results, and any deviations from the specification.", `Write to: ${specDoc(s, "implementation-summary")}`, "", "Output <control> JSON with: docPath, phasesCompleted, allGreen, summary."].join("\n");
101
+ }
102
+ export function buildCodeReviewPrompt(s: SetupControl, c: Classification | null, task: string, specControl: R, implControl: R): string {
103
+ return [ctxBlock(s, c), "", "## Upstream Artifacts", `- Specification: ${(specControl?.specificationPath as string) ?? "N/A"}`, `- Phases Completed: ${(implControl?.phasesCompleted as number) ?? 0}/${(implControl?.totalPhases as number) ?? 0}`, "", "## Task", task, "", "## Instructions", "Review the implementation against the specification for correctness, security, performance, and maintainability.", "Produce a verdict and list findings with severity.", `Write the review to: ${specDoc(s, "code-review")}`, "", "Output <control> JSON with: docPath, verdict ('Approved'|'Approved with Comments'|'Changes Requested'), findings (array), dimensionsCovered (array), summary."].join("\n");
104
+ }
105
+ export function buildAdversarialPrompt(s: SetupControl, c: Classification | null, task: string, specControl: R, implControl: R): string {
106
+ return [ctxBlock(s, c), "", "## Upstream Artifacts", `- Specification: ${(specControl?.specificationPath as string) ?? "N/A"}`, `- Phases Completed: ${(implControl?.phasesCompleted as number) ?? 0}/${(implControl?.totalPhases as number) ?? 0}`, "", "## Task", task, "", "## Instructions", "Challenge the implementation from three critical lenses: Skeptic, Architect, Minimalist.", "Look for issues standard review misses: over-engineering, hidden complexity, missing error paths.", `Write the review to: ${specDoc(s, "adversarial-review")}`, "", "Output <control> JSON with: docPath, verdict ('Approved'|'Approved with Comments'|'Changes Requested'), findings (array), dimensionsCovered (array), summary."].join("\n");
107
+ }
108
+ export function buildFixPrompt(s: SetupControl, c: Classification | null, findings: unknown[]): string {
109
+ const list = (findings ?? []).map((f) => { const o = f as { severity?: string; title?: string; message?: string }; return `- [${o.severity ?? "medium"}] ${o.title ?? o.message ?? JSON.stringify(f)}`; }).join("\n");
110
+ return [ctxBlock(s, c), "", "## Code Review Findings to Address", list || "- (no specific findings)", "", "## Instructions", "Fix the issues identified in code review. Make minimal, targeted changes.", "Run tests after each fix to ensure no regressions.", "", "Output <control> JSON with: filesModified (array), fixesApplied (number), summary."].join("\n");
111
+ }
112
+ export function buildDocsPrompt(s: SetupControl, c: Classification | null, task: string, specControl: R): string {
113
+ return [ctxBlock(s, c), "", "## Task", task, "", "## Upstream Artifacts", `- Specification: ${(specControl?.specificationPath as string) ?? "N/A"}`, `- Spec Directory: ${s.specDirectory}`, "", "## Instructions", "Update documentation to reflect the implementation:", "- Review spec directory files for accuracy against the code", "- Update README, CHANGELOG, API docs as needed", "- Document any deviations from the specification", `Write a summary of documentation changes to: ${specDoc(s, "documentation")}`, "", "Output <control> JSON with: docPath, docsUpdated (boolean), specDirFilesReviewed (array), deviationsDocumented (array), summary."].join("\n");
114
+ }
115
+ export function buildCommitPrompt(s: SetupControl, phaseName: string): string {
116
+ return ["## Context", `- Worktree: ${s.worktreePath}`, "", "## Instructions", `Commit all changes for implementation phase: ${phaseName}`, "Use a conventional commit message that describes the phase work.", "Stage only files relevant to this phase."].join("\n");
117
+ }
118
+ export function buildMergePrompt(s: SetupControl): string {
119
+ return ["## Context", `- Worktree: ${s.worktreePath}`, `- Default Branch: ${s.defaultBranch ?? "main"}`, "", "## Instructions", "Merge the feature branch back into the default branch.", "Ensure all changes are committed. Create a merge commit with a summary of all work done.", "If there are conflicts, resolve them preserving the feature branch changes.", "", "Output <control> JSON with: merged (boolean), commitSha, mergeCommand, summary."].join("\n");
120
+ }