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,35 @@
1
+ ---
2
+ name: super-dev
3
+ description: Self-contained 13-stage development pipeline built on a composable control-flow node algebra (branch/parallel/loop/retry/gate). Orchestrates requirements, research, design, specification, TDD implementation, code review, documentation, and merge through 21 specialist agents spawned directly as `pi` subprocesses. No external workflow engine required.
4
+ ---
5
+
6
+ # Super Dev
7
+
8
+ Use this skill when the user asks to implement a feature, fix a bug, refactor code, or do systematic multi-stage development work.
9
+
10
+ ## When to use
11
+
12
+ Triggers: "implement", "build", "fix bug", "refactor", "add feature", "develop this", "help me build", "optimize performance", "resolve deprecation".
13
+
14
+ Do NOT trigger on: simple questions, file searches, one-off commands, code explanations, quick edits.
15
+
16
+ ## Action
17
+
18
+ Use the `super_dev` tool to start the pipeline. It spawns 21 specialist `pi` subagents directly — there is no `workflow_run` tool and no dependency on pi-workflow.
19
+
20
+ ```text
21
+ super_dev({ task: "<user's full request>" })
22
+ ```
23
+
24
+ Optional flags:
25
+ - `skipWorktree: true` — operate in the current directory instead of a git worktree.
26
+ - `model: "provider/id"` — override the model used by spawned specialists.
27
+ - `maxAgents: 200` — cap total specialist spawns.
28
+
29
+ Preserve the user's language, file references, and constraints verbatim in the `task`.
30
+
31
+ The user can also invoke the pipeline via the `/super-dev` command:
32
+
33
+ ```text
34
+ /super-dev <task description>
35
+ ```
package/src/agents.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Loads specialist agent system prompts from `agents/<name>.md`.
3
+ * The YAML frontmatter is metadata; the body is passed to spawned `pi` via
4
+ * `--system-prompt`.
5
+ */
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
12
+ const AGENTS_DIR = join(MODULE_DIR, "..", "agents");
13
+ const cache = new Map<string, string>();
14
+
15
+ function stripFrontmatter(md: string): string {
16
+ if (!md.startsWith("---")) return md;
17
+ const end = md.indexOf("\n---", 3);
18
+ if (end === -1) return md;
19
+ return md.slice(end + 4).replace(/^\s*\n/, "");
20
+ }
21
+
22
+ export function loadAgentPrompt(name: string): string {
23
+ const cached = cache.get(name);
24
+ if (cached !== undefined) return cached;
25
+ const path = join(AGENTS_DIR, `${name}.md`);
26
+ let body: string;
27
+ try {
28
+ body = stripFrontmatter(readFileSync(path, "utf8"));
29
+ } catch {
30
+ throw new Error(`super-dev: unknown agent "${name}" (no file at ${path})`);
31
+ }
32
+ cache.set(name, body);
33
+ return body;
34
+ }
35
+
36
+ export function agentsDirectory(): string {
37
+ return AGENTS_DIR;
38
+ }
package/src/control.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Tolerant extraction of the `<control>` JSON object specialist agents emit.
3
+ * Tries, in order: `<control>...</control>` tag, ```json fenced block, then the
4
+ * last balanced `{...}` object in the text. Returns null if none parse.
5
+ */
6
+
7
+ import type { ControlObj } from "./types.ts";
8
+
9
+ const CONTROL_TAG_RE = /<control>\s*([\s\S]*?)\s<\/control>/i;
10
+
11
+ export function extractControl(text: string): ControlObj | null {
12
+ if (!text) return null;
13
+ const tag = text.match(CONTROL_TAG_RE);
14
+ if (tag?.[1]) {
15
+ const parsed = tryParseJsonObject(tag[1]);
16
+ if (parsed) return parsed;
17
+ }
18
+ for (const match of text.matchAll(/```(?:json)?\s*([\s\S]*?)\s```/gi)) {
19
+ const parsed = tryParseJsonObject(match[1]);
20
+ if (parsed) return parsed;
21
+ }
22
+ const obj = findLastJsonObject(text);
23
+ if (obj) {
24
+ const parsed = tryParseJsonObject(obj);
25
+ if (parsed) return parsed;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function tryParseJsonObject(raw: string): ControlObj | null {
31
+ const trimmed = raw.replace(/,(\s*[}\]])/g, "$1").trim();
32
+ if (!trimmed.startsWith("{")) return null;
33
+ try {
34
+ const value = JSON.parse(trimmed);
35
+ if (value && typeof value === "object" && !Array.isArray(value)) {
36
+ return value as ControlObj;
37
+ }
38
+ } catch {
39
+ // fall through
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /** The list of control keys a stage expects, parsed from its prompt.
45
+ * Every `build*Prompt` ends with a line like:
46
+ * Output <control> JSON with: docPath, featureName, acCount, openQuestions, summary.
47
+ * We parse that comma-list (stripping inline `(type)` annotations) so the
48
+ * session backend can declare those keys in its `structured_output` tool
49
+ * schema — which is what actually makes the model fill them (see
50
+ * docs/findings/session-backend-requirements-gate.md). Returns [] if the
51
+ * prompt has no such line (e.g. commit tasks), which safely degrades to the
52
+ * permissive schema. */
53
+ export function extractControlKeys(prompt: string): string[] {
54
+ const m = prompt.match(/<control>\s*JSON\s*with:\s*([^\n.]+)/i);
55
+ if (!m) return [];
56
+ return m[1]
57
+ .split(",")
58
+ .map((s) => s.replace(/\([^)]*\)/g, "").trim())
59
+ .filter((s) => /^[A-Za-z_][\w]*$/.test(s));
60
+ }
61
+
62
+ /** Find the last balanced `{...}` substring via a brace scan. */
63
+ export function findLastJsonObject(text: string): string | null {
64
+ const lastOpen = text.lastIndexOf("{");
65
+ if (lastOpen === -1) return null;
66
+ let depth = 0;
67
+ let inString = false;
68
+ let escape = false;
69
+ for (let i = lastOpen; i < text.length; i++) {
70
+ const ch = text[i];
71
+ if (inString) {
72
+ if (escape) escape = false;
73
+ else if (ch === "\\") escape = true;
74
+ else if (ch === '"') inString = false;
75
+ continue;
76
+ }
77
+ if (ch === '"') inString = true;
78
+ else if (ch === "{") depth++;
79
+ else if (ch === "}") {
80
+ depth--;
81
+ if (depth === 0) return text.slice(lastOpen, i + 1);
82
+ }
83
+ }
84
+ return null;
85
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Doc-content validators for the spec-stage gates.
3
+ *
4
+ * The gates in helpers.ts used to trust the agent's self-reported control JSON
5
+ * (scenarioCount, coverageScore, …). That was fragile: models return numbers
6
+ * as strings ("13"), omit keys, or self-report scores that don't match the doc.
7
+ * A real /super-dev run wrote an excellent 26-scenario BDD doc but the gate
8
+ * failed on the control object's shape — a false negative.
9
+ *
10
+ * These validators read the ACTUAL .md file the agent wrote and check its
11
+ * content (regex / min-size), ported from the original super-dev-plugin's
12
+ * scripts/gates/definitions.mjs. Gates prefer this; the old metadata checks
13
+ * survive only as a fallback when no doc can be found on disk.
14
+ */
15
+
16
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import type { ControlObj } from "./types.ts";
19
+
20
+ export interface DocRef {
21
+ path: string;
22
+ content: string;
23
+ }
24
+
25
+ /** Convert a simple filename glob ("*-bdd-scenarios.md") into a RegExp. */
26
+ function globToRegExp(glob: string): RegExp {
27
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
28
+ return new RegExp(`^${escaped}$`, "i");
29
+ }
30
+
31
+ /** Count regex matches in content (ensures the `g` flag so match() counts all). */
32
+ function countMatches(content: string, re: RegExp): number {
33
+ const global = re.flags.includes("g") ? re : new RegExp(re.source, re.flags + "g");
34
+ return (content.match(global) ?? []).length;
35
+ }
36
+
37
+ /**
38
+ * Locate & read a stage's doc. Prefer an explicitly-declared path in the control
39
+ * object (docPath / specificationPath / …); fall back to a glob of the spec
40
+ * directory so the gate still works when the agent omits or misreports the path.
41
+ * Returns null if no doc can be found.
42
+ */
43
+ export function readSpecDoc(specDir: string, control: ControlObj | undefined, glob: string, pathKeys: string[] = ["docPath"]): DocRef | null {
44
+ for (const k of pathKeys) {
45
+ const p = control?.[k];
46
+ if (typeof p === "string" && p && existsSync(p)) {
47
+ return { path: p, content: readFileSync(p, "utf8") };
48
+ }
49
+ }
50
+ if (specDir) {
51
+ try {
52
+ const re = globToRegExp(glob);
53
+ for (const entry of readdirSync(specDir)) {
54
+ if (re.test(entry)) {
55
+ const p = join(specDir, entry);
56
+ if (existsSync(p)) return { path: p, content: readFileSync(p, "utf8") };
57
+ }
58
+ }
59
+ } catch { /* spec dir unreadable — fall through */ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /** True if a sibling doc exists in the spec dir (for file-existence checks). */
65
+ export function specDocExists(specDir: string, glob: string): boolean {
66
+ if (!specDir) return false;
67
+ try {
68
+ const re = globToRegExp(glob);
69
+ return readdirSync(specDir).some((e) => re.test(e));
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // ─── coercion: models return "13" / "true" where a gate wants 13 / true ───────
76
+
77
+ export function toNumber(v: unknown): number | null {
78
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
79
+ if (typeof v === "string") {
80
+ const n = Number(v.trim());
81
+ return Number.isFinite(n) ? n : null;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ export function toBool(v: unknown): boolean {
87
+ if (typeof v === "boolean") return v;
88
+ if (typeof v === "string") return /^(true|yes|y|1|pass)$/i.test(v.trim());
89
+ return false;
90
+ }
91
+
92
+ /** Normalize a spec's `phases` field into a usable {name, description?} array.
93
+ * Agents occasionally return phases as a string (newline/comma list) or an
94
+ * object instead of an array; the implementation stage iterates it, so a
95
+ * non-array must never reach `for...of phases.entries()` (which threw:
96
+ * "phases.entries is not a function"). Array → keep valid entries; string →
97
+ * best-effort split into names; anything else → []. */
98
+ export function normalizePhases(raw: unknown): Array<{ name: string; description?: string }> {
99
+ if (Array.isArray(raw)) {
100
+ return raw.filter((p): p is { name: string; description?: string } =>
101
+ !!p && typeof p === "object" && typeof (p as { name?: unknown }).name === "string" && (p as { name: string }).name.trim() !== "",
102
+ );
103
+ }
104
+ if (typeof raw === "string" && raw.trim()) {
105
+ return raw
106
+ .split(/\r?\n|,|;|•/)
107
+ .map((x) => x.trim().replace(/^[-*\d.)\s]+/, "").trim())
108
+ .filter((x) => x.length > 0)
109
+ .map((name) => ({ name }));
110
+ }
111
+ return [];
112
+ }
113
+
114
+ /** Tolerant approved-verdict test. Accepts Approved / Approved with Comments /
115
+ * Approved with minor changes / PASS / Accepted (any case); rejects Changes
116
+ * Requested / Rejected / CONTEST / Blocked / FAIL. */
117
+ export function isApprovedVerdict(verdict: unknown): boolean {
118
+ const v = String(verdict ?? "").trim().toLowerCase();
119
+ if (/(changes?\s+requested|reject|contest|blocked|fail|revision|declined)/i.test(v)) return false;
120
+ return /\b(approved|pass|accept)/i.test(v);
121
+ }
122
+
123
+ // ─── per-stage content checks (ported from definitions.mjs) ──────────────────
124
+ // Each returns a list of human-readable errors; empty = doc content is valid.
125
+
126
+ /** requirements.md: acceptance criteria, AC items, NFRs, summary, substance. */
127
+ export function requirementsContentErrors(c: string): string[] {
128
+ const e: string[] = [];
129
+ if (countMatches(c, /acceptance\s+criteria/i) < 1) e.push("missing an 'Acceptance Criteria' section");
130
+ if (countMatches(c, /AC-\d+/g) < 2) e.push("needs ≥2 acceptance-criteria items (AC-NN)");
131
+ if (countMatches(c, /non-functional|performance|security|accessibility/i) < 1) e.push("missing non-functional requirements");
132
+ if (countMatches(c, /executive\s+summary|##\s+summary|\bsummary\b/i) < 1) e.push("missing a summary section");
133
+ if (c.length < 500) e.push("doc is too short (<500 bytes) — likely a stub");
134
+ return e;
135
+ }
136
+
137
+ /** bdd-scenarios.md: SCENARIO-NN ids, Given/When/Then, AC traceability, substance. */
138
+ export function bddContentErrors(c: string): string[] {
139
+ const e: string[] = [];
140
+ if (countMatches(c, /SCENARIO-\d+/g) < 1) e.push("missing SCENARIO-NN identifiers");
141
+ // Given/When/Then keyword lines (tolerant of bullets/bold), ≥3 distinct blocks
142
+ if (countMatches(c, /^\s*(?:[-*]\s+)?\*{0,2}(?:given|when|then|and)\b/im) < 3) e.push("missing Given/When/Then structure (≥3 blocks)");
143
+ if (countMatches(c, /AC-\d+/g) < 1) e.push("missing AC references for traceability");
144
+ if (c.length < 300) e.push("doc is too short (<300 bytes) — likely a stub");
145
+ return e;
146
+ }
147
+
148
+ /** specification.md: BDD scenario refs, testing strategy, substance. */
149
+ export function specContentErrors(c: string): string[] {
150
+ const e: string[] = [];
151
+ if (countMatches(c, /SCENARIO-\d+/g) < 1) e.push("specification must reference BDD scenarios (SCENARIO-NN)");
152
+ if (countMatches(c, /testing\s+strategy|test\s+plan|test\s+approach|test\s+coverage|unit\s+test|integration\s+test|e2e\s+test/i) < 1) e.push("missing a testing strategy");
153
+ if (c.length < 500) e.push("specification is too short (<500 bytes) — likely a stub");
154
+ return e;
155
+ }
156
+
157
+ /** spec-review.md: all 8 review dimensions present. */
158
+ export function specReviewContentErrors(c: string): string[] {
159
+ const e: string[] = [];
160
+ const dims = ["Completeness", "Consistency", "Feasibility", "Testability", "Traceability", "Grounding", "Complexity", "Ambiguity"];
161
+ const found = dims.filter((d) => new RegExp(d, "i").test(c));
162
+ if (found.length < 8) e.push(`missing review dimensions (${found.length}/8: ${found.join(", ") || "none"})`);
163
+ return e;
164
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Pi extension entry point.
3
+ *
4
+ * Registers:
5
+ * - `super_dev` tool — the LLM-callable entry that runs the 13-stage
6
+ * pipeline by spawning `pi` child processes. Fully self-contained: no
7
+ * dependency on @agwab/pi-workflow or any other workflow engine. The
8
+ * pipeline is a tree of control-flow nodes (src/nodes.ts) composed in
9
+ * src/stages/index.ts.
10
+ * - `/super-dev <task>` command — dispatches the task to the agent, which
11
+ * invokes the `super_dev` tool.
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { Type } from "typebox";
16
+ import { mkdirSync, writeFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { runPipelineTask } from "./pipeline.ts";
19
+ import { abbreviatePath } from "./pi-spawn.ts";
20
+ import type { ProgressSink, RunStatus, RunSummary } from "./types.ts";
21
+
22
+ export { runPipelineTask } from "./pipeline.ts";
23
+ export { SUPER_DEV_WORKFLOW } from "./stages/index.ts";
24
+ export * as nodes from "./nodes.ts";
25
+ export { runWorkflow } from "./workflow.ts";
26
+
27
+ const SUPER_DEV_TOOL = "super_dev";
28
+ const SUPER_DEV_COMMAND = "super-dev";
29
+
30
+ /** Format a run summary honestly: success ✅ / partial ⚠️ / failed ❌. */
31
+ function formatSummary(s: RunSummary, cwd?: string): string[] {
32
+ const icon: Record<RunStatus, string> = { success: "✅", partial: "⚠️", failed: "❌" };
33
+ const title: Record<RunStatus, string> = {
34
+ success: "super-dev pipeline complete",
35
+ partial: "super-dev pipeline completed with issues",
36
+ failed: "super-dev pipeline did NOT complete",
37
+ };
38
+ const impl = s.state.implementation as { summary?: string; totalPhases?: number; allGreen?: boolean } | undefined;
39
+ const review = s.state.review as { verdict?: string } | undefined;
40
+ const setup = s.state.setup as { language?: string; isWebUi?: boolean; defaultBranch?: string; worktreeCreated?: boolean; initializedRepo?: boolean } | undefined;
41
+ const classify = s.state.classify as { taskType?: string; uiScope?: string } | undefined;
42
+ const lines = [
43
+ `${icon[s.status]} ${title[s.status]}`,
44
+ ` Spec: ${s.specIdentifier || "(none)"}`,
45
+ ` Worktree: ${abbreviatePath(s.worktreePath, cwd)}${setup?.worktreeCreated ? " (created)" : setup ? " (in-place)" : ""}`,
46
+ ` Stack: ${setup ? `${setup.language}${setup.isWebUi ? " | Web UI" : ""}${setup.defaultBranch ? ` | branch ${setup.defaultBranch}` : ""}` : "n/a"}`,
47
+ ` Classify: ${classify ? `${classify.taskType}${classify.uiScope ? ` | ${classify.uiScope}` : ""}` : "n/a"}`,
48
+ ` Agents: ${s.agentsSpawned} spawned`,
49
+ ` Impl: ${impl?.summary ?? (impl ? `${impl.totalPhases ?? 0} phase(s), allGreen=${impl.allGreen ?? false}` : "none produced")}`,
50
+ ` Review: ${review?.verdict ?? (s.state.review ? "no verdict" : "skipped")}`,
51
+ ` Merged: ${s.state.merge ? String((s.state.merge as { merged?: boolean }).merged ?? false) : "skipped"}`,
52
+ ];
53
+ if (s.failedStages.length > 0) {
54
+ const fmt = (f: { label: string; error?: string }) => {
55
+ const e = f.error ? ` — ${f.error}` : "";
56
+ return `${f.label}${e}`;
57
+ };
58
+ lines.push(` Failed: ${s.failedStages.map(fmt).join("\n ")}`);
59
+ }
60
+ if (s.error) lines.push(` Error: ${s.error}`);
61
+ return lines;
62
+ }
63
+
64
+ export default function activate(pi: ExtensionAPI): void {
65
+ pi.registerTool({
66
+ name: SUPER_DEV_TOOL,
67
+ label: "Super Dev",
68
+ description:
69
+ "Run the self-contained 13-stage super-dev pipeline (requirements → research → design → spec → TDD implementation → code review → docs → merge). Spawns specialist `pi` subagents directly — no external workflow engine required.",
70
+ promptSnippet: "Run the full 13-stage super-dev development pipeline for a feature/bug/refactor task",
71
+ promptGuidelines: [
72
+ "Use super_dev when the user asks to implement a feature, fix a bug, or refactor code as a structured multi-stage workflow.",
73
+ "Pass the user's full task verbatim to super_dev; do not paraphrase constraints, file references, or acceptance criteria.",
74
+ ],
75
+ parameters: Type.Object({
76
+ task: Type.String({ description: "The full development task, e.g. 'implement OAuth2 login' or 'fix the crash on large file upload'." }),
77
+ skipWorktree: Type.Optional(Type.Boolean({ description: "Skip git worktree creation and operate in the current directory. Default: false." })),
78
+ skipStages: Type.Optional(Type.Array(Type.String(), { description: "Stage output keys to skip (advanced). Default: none." })),
79
+ model: Type.Optional(Type.String({ description: "Model override for spawned specialist agents in provider/id form." })),
80
+ maxAgents: Type.Optional(Type.Number({ description: "Maximum specialist agent spawns. Default: 200." })),
81
+ }),
82
+ async execute(_toolCallId, params, signal, onUpdate) {
83
+ const task = String(params.task ?? "").trim();
84
+ if (!task) {
85
+ return { content: [{ type: "text", text: "super_dev requires a non-empty `task`." }], isError: true, details: {} };
86
+ }
87
+ const transcript: string[] = [];
88
+ let live = "";
89
+ let lastFlush = 0;
90
+ const FLUSH_MS = 80;
91
+ // The live display is a ROLLING TAIL. A full run (100+ agents) produces
92
+ // thousands of transcript lines; sending the whole thing on every flush
93
+ // let pi truncate it, and since later stages append at the END, they were
94
+ // the first to fall off the visible window ("very little logs afterwards").
95
+ // The tail keeps the CURRENT activity visible; the full log is written to
96
+ // disk at run end so nothing is lost.
97
+ const TAIL_LINES = 400;
98
+ const finalizeLive = () => {
99
+ if (live) {
100
+ transcript.push(live);
101
+ live = "";
102
+ }
103
+ };
104
+ const flush = () => {
105
+ const all = live ? [...transcript, live] : transcript;
106
+ const body = all.length > TAIL_LINES
107
+ ? `… ${all.length - TAIL_LINES} earlier lines trimmed (full log saved to .super-dev-logs/ at run end) …\n` + all.slice(-TAIL_LINES).join("\n")
108
+ : all.join("\n");
109
+ onUpdate?.({ content: [{ type: "text", text: body }], details: {} });
110
+ };
111
+ const sink: ProgressSink = {
112
+ phase: (label) => { finalizeLive(); transcript.push(`▶ ${label}`); flush(); },
113
+ log: (message) => { finalizeLive(); transcript.push(` ${message}`); flush(); },
114
+ text: (partial) => {
115
+ live = partial;
116
+ const now = Date.now();
117
+ if (now - lastFlush >= FLUSH_MS) { flush(); lastFlush = now; }
118
+ },
119
+ };
120
+ try {
121
+ const summary = await runPipelineTask(task, {
122
+ cwd: process.cwd(),
123
+ skipWorktree: params.skipWorktree === true,
124
+ skipStages: params.skipStages as string[] | undefined,
125
+ model: params.model as string | undefined,
126
+ maxAgents: typeof params.maxAgents === "number" ? params.maxAgents : undefined,
127
+ progress: sink,
128
+ signal,
129
+ });
130
+ const lines = formatSummary(summary, process.cwd());
131
+ // Preserve the FULL run log to disk (the live display is a rolling tail).
132
+ let logPath = "";
133
+ try {
134
+ const logDir = join(process.cwd(), ".super-dev-logs");
135
+ mkdirSync(logDir, { recursive: true });
136
+ logPath = join(logDir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${summary.specIdentifier || "run"}.log`);
137
+ writeFileSync(logPath, transcript.join("\n") + "\n");
138
+ } catch { /* best-effort; the live tail is the primary surface */ }
139
+ if (logPath) lines.push(`Full run log: ${logPath}`);
140
+ const isError = summary.status === "failed";
141
+ return { content: [{ type: "text", text: lines.join("\n") }], isError, details: { summary } };
142
+ } catch (err) {
143
+ const message = err instanceof Error ? err.message : String(err);
144
+ return { content: [{ type: "text", text: `❌ super-dev pipeline failed: ${message}` }], isError: true, details: {} };
145
+ }
146
+ },
147
+ });
148
+
149
+ pi.registerCommand(SUPER_DEV_COMMAND, {
150
+ description: "Run the 13-stage super-dev pipeline. Usage: /super-dev <task description>",
151
+ handler: async (args, ctx) => {
152
+ const task = String(args ?? "").trim();
153
+ if (!task) {
154
+ ctx.ui.notify(
155
+ "Usage: /super-dev <task description>\n\nExamples:\n /super-dev implement user authentication with OAuth2\n /super-dev fix the crash when uploading large files",
156
+ "info",
157
+ );
158
+ return;
159
+ }
160
+ // Dispatch to the agent so it runs interruptibly and the tool streams progress.
161
+ pi.sendUserMessage(`Use the ${SUPER_DEV_TOOL} tool to run the full super-dev pipeline for this task: ${task}`);
162
+ },
163
+ });
164
+ }