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
|
@@ -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
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -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
|
+
}
|