supipowers 0.2.7 → 0.4.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/package.json +21 -6
- package/skills/debugging/SKILL.md +54 -15
- package/skills/fix-pr/SKILL.md +99 -0
- package/skills/planning/SKILL.md +70 -10
- package/skills/receiving-code-review/SKILL.md +87 -0
- package/skills/tdd/SKILL.md +83 -0
- package/skills/verification/SKILL.md +54 -0
- package/src/commands/fix-pr.ts +324 -0
- package/src/commands/plan.ts +96 -31
- package/src/commands/qa.ts +150 -29
- package/src/commands/release.ts +1 -1
- package/src/commands/review.ts +2 -2
- package/src/commands/run.ts +52 -2
- package/src/commands/supi.ts +1 -0
- package/src/commands/update.ts +2 -2
- package/src/discipline/debugging.ts +57 -0
- package/src/discipline/receiving-review.ts +65 -0
- package/src/discipline/tdd.ts +77 -0
- package/src/discipline/verification.ts +68 -0
- package/src/fix-pr/config.ts +36 -0
- package/src/fix-pr/prompt-builder.ts +201 -0
- package/src/fix-pr/scripts/diff-comments.sh +33 -0
- package/src/fix-pr/scripts/fetch-pr-comments.sh +25 -0
- package/src/fix-pr/scripts/trigger-review.sh +36 -0
- package/src/fix-pr/scripts/wait-and-check.sh +37 -0
- package/src/fix-pr/types.ts +71 -0
- package/src/git/branch-finish.ts +101 -0
- package/src/git/worktree.ts +119 -0
- package/src/index.ts +13 -2
- package/src/lsp/detector.ts +2 -2
- package/src/orchestrator/agent-prompts.ts +282 -0
- package/src/orchestrator/dispatcher.ts +150 -1
- package/src/orchestrator/prompts.ts +17 -31
- package/src/planning/plan-reviewer.ts +49 -0
- package/src/planning/plan-writer-prompt.ts +173 -0
- package/src/planning/prompt-builder.ts +178 -0
- package/src/planning/spec-reviewer.ts +43 -0
- package/src/qa/phases/discovery.ts +34 -0
- package/src/qa/phases/execution.ts +65 -0
- package/src/qa/phases/matrix.ts +41 -0
- package/src/qa/phases/reporting.ts +71 -0
- package/src/qa/session.ts +104 -0
- package/src/storage/fix-pr-sessions.ts +59 -0
- package/src/storage/qa-sessions.ts +83 -0
- package/src/storage/specs.ts +36 -0
- package/src/types.ts +70 -0
- package/src/visual/companion.ts +115 -0
- package/src/visual/prompt-instructions.ts +102 -0
- package/src/visual/scripts/frame-template.html +201 -0
- package/src/visual/scripts/helper.js +88 -0
- package/src/visual/scripts/index.js +148 -0
- package/src/visual/scripts/package.json +10 -0
- package/src/visual/scripts/start-server.sh +98 -0
- package/src/visual/scripts/stop-server.sh +21 -0
- package/src/visual/types.ts +16 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { loadFixPrConfig, saveFixPrConfig, DEFAULT_FIX_PR_CONFIG } from "../fix-pr/config.js";
|
|
5
|
+
import { buildFixPrOrchestratorPrompt } from "../fix-pr/prompt-builder.js";
|
|
6
|
+
import type { FixPrConfig, ReviewerType, CommentReplyPolicy } from "../fix-pr/types.js";
|
|
7
|
+
import {
|
|
8
|
+
generateFixPrSessionId,
|
|
9
|
+
createFixPrSession,
|
|
10
|
+
findActiveFixPrSession,
|
|
11
|
+
getSessionDir,
|
|
12
|
+
} from "../storage/fix-pr-sessions.js";
|
|
13
|
+
import { notifyInfo, notifyError, notifyWarning } from "../notifications/renderer.js";
|
|
14
|
+
|
|
15
|
+
function getScriptsDir(): string {
|
|
16
|
+
return path.join(path.dirname(new URL(import.meta.url).pathname), "..", "fix-pr", "scripts");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findSkillPath(skillName: string): string | null {
|
|
20
|
+
const candidates = [
|
|
21
|
+
path.join(process.cwd(), "skills", skillName, "SKILL.md"),
|
|
22
|
+
path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "skills", skillName, "SKILL.md"),
|
|
23
|
+
];
|
|
24
|
+
for (const p of candidates) {
|
|
25
|
+
if (fs.existsSync(p)) return p;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerFixPrCommand(pi: ExtensionAPI): void {
|
|
31
|
+
pi.registerCommand("supi:fix-pr", {
|
|
32
|
+
description: "Fix PR review comments with token-optimized agent orchestration",
|
|
33
|
+
async handler(args, ctx) {
|
|
34
|
+
// ── Step 1: Detect PR ──────────────────────────────────────────
|
|
35
|
+
let prNumber: number | null = null;
|
|
36
|
+
let repo: string | null = null;
|
|
37
|
+
|
|
38
|
+
// Try to parse from args
|
|
39
|
+
const argTrimmed = args?.trim().replace("#", "") || "";
|
|
40
|
+
if (/^\d+$/.test(argTrimmed)) {
|
|
41
|
+
prNumber = parseInt(argTrimmed, 10);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Detect repo
|
|
45
|
+
try {
|
|
46
|
+
const repoResult = await pi.exec("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], { cwd: ctx.cwd });
|
|
47
|
+
if (repoResult.code === 0) repo = repoResult.stdout.trim();
|
|
48
|
+
} catch { /* ignore */ }
|
|
49
|
+
|
|
50
|
+
if (!repo) {
|
|
51
|
+
notifyError(ctx, "Could not detect repository", "Run from a git repo with gh CLI configured");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Detect PR number from current branch if not provided
|
|
56
|
+
if (!prNumber) {
|
|
57
|
+
try {
|
|
58
|
+
const prResult = await pi.exec("gh", ["pr", "view", "--json", "number", "-q", ".number"], { cwd: ctx.cwd });
|
|
59
|
+
if (prResult.code === 0) prNumber = parseInt(prResult.stdout.trim(), 10);
|
|
60
|
+
} catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!prNumber) {
|
|
64
|
+
notifyError(ctx, "No PR found", "Provide PR number as argument or run from a PR branch");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Step 2: Load or create config ──────────────────────────────
|
|
69
|
+
let config = loadFixPrConfig(ctx.cwd);
|
|
70
|
+
|
|
71
|
+
if (!config && ctx.hasUI) {
|
|
72
|
+
config = await runSetupWizard(ctx);
|
|
73
|
+
if (!config) return; // user cancelled
|
|
74
|
+
saveFixPrConfig(ctx.cwd, config);
|
|
75
|
+
ctx.ui.notify("Fix-PR config saved to .omp/supipowers/fix-pr.json", "info");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!config) {
|
|
79
|
+
notifyError(ctx, "No fix-pr config", "Run interactively first to set up configuration");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Step 3: Session handling ───────────────────────────────────
|
|
84
|
+
let activeSession = findActiveFixPrSession(ctx.cwd);
|
|
85
|
+
|
|
86
|
+
if (activeSession && ctx.hasUI) {
|
|
87
|
+
const choice = await ctx.ui.select(
|
|
88
|
+
"Fix-PR Session",
|
|
89
|
+
[
|
|
90
|
+
`Resume ${activeSession.id} (iteration ${activeSession.iteration}, PR #${activeSession.prNumber})`,
|
|
91
|
+
"Start new session",
|
|
92
|
+
],
|
|
93
|
+
{ helpText: "Select session · Esc to cancel" },
|
|
94
|
+
);
|
|
95
|
+
if (!choice) return;
|
|
96
|
+
if (choice.startsWith("Start new")) activeSession = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ledger = activeSession ?? {
|
|
100
|
+
id: generateFixPrSessionId(),
|
|
101
|
+
createdAt: new Date().toISOString(),
|
|
102
|
+
updatedAt: new Date().toISOString(),
|
|
103
|
+
prNumber,
|
|
104
|
+
repo,
|
|
105
|
+
status: "running" as const,
|
|
106
|
+
iteration: 0,
|
|
107
|
+
config,
|
|
108
|
+
commentsProcessed: [],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (!activeSession) {
|
|
112
|
+
createFixPrSession(ctx.cwd, ledger);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Step 4: Fetch initial comments ─────────────────────────────
|
|
116
|
+
const sessionDir = getSessionDir(ctx.cwd, ledger.id);
|
|
117
|
+
const scriptsDir = getScriptsDir();
|
|
118
|
+
const snapshotPath = path.join(sessionDir, "snapshots", `comments-${ledger.iteration}.jsonl`);
|
|
119
|
+
|
|
120
|
+
const fetchResult = await pi.exec("bash", [
|
|
121
|
+
path.join(scriptsDir, "fetch-pr-comments.sh"),
|
|
122
|
+
repo,
|
|
123
|
+
String(prNumber),
|
|
124
|
+
snapshotPath,
|
|
125
|
+
], { cwd: ctx.cwd });
|
|
126
|
+
|
|
127
|
+
if (fetchResult.code !== 0) {
|
|
128
|
+
notifyError(ctx, "Failed to fetch PR comments", fetchResult.stderr);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Read the snapshot
|
|
133
|
+
let comments = "";
|
|
134
|
+
try {
|
|
135
|
+
comments = fs.readFileSync(snapshotPath, "utf-8").trim();
|
|
136
|
+
} catch {
|
|
137
|
+
notifyWarning(ctx, "No comments found", "PR has no review comments to process");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!comments) {
|
|
142
|
+
notifyInfo(ctx, "No comments to process", "PR has no review comments");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const commentCount = comments.split("\n").length;
|
|
147
|
+
|
|
148
|
+
// ── Step 5: Load skill ─────────────────────────────────────────
|
|
149
|
+
let skillContent = "";
|
|
150
|
+
const skillPath = findSkillPath("fix-pr");
|
|
151
|
+
if (skillPath) {
|
|
152
|
+
try {
|
|
153
|
+
skillContent = fs.readFileSync(skillPath, "utf-8");
|
|
154
|
+
} catch { /* proceed without */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Step 6: Build and send prompt ──────────────────────────────
|
|
158
|
+
const prompt = buildFixPrOrchestratorPrompt({
|
|
159
|
+
prNumber,
|
|
160
|
+
repo,
|
|
161
|
+
comments,
|
|
162
|
+
sessionDir,
|
|
163
|
+
scriptsDir,
|
|
164
|
+
config,
|
|
165
|
+
iteration: ledger.iteration,
|
|
166
|
+
skillContent,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
pi.sendMessage(
|
|
170
|
+
{
|
|
171
|
+
customType: "supi-fix-pr",
|
|
172
|
+
content: [{ type: "text", text: prompt }],
|
|
173
|
+
display: "none",
|
|
174
|
+
},
|
|
175
|
+
{ deliverAs: "steer" },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
notifyInfo(ctx, `Fix-PR started: PR #${prNumber}`, `${commentCount} comments to assess | session ${ledger.id}`);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Setup Wizard ───────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
const REVIEWER_OPTIONS = [
|
|
186
|
+
"CodeRabbit",
|
|
187
|
+
"GitHub Copilot",
|
|
188
|
+
"Gemini Code Review",
|
|
189
|
+
"None",
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const REVIEWER_DEFAULTS: Record<string, string> = {
|
|
193
|
+
"CodeRabbit": "/review",
|
|
194
|
+
"GitHub Copilot": "@copilot review",
|
|
195
|
+
"Gemini Code Review": "/gemini review",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const POLICY_OPTIONS = [
|
|
199
|
+
"Answer all comments",
|
|
200
|
+
"Only answer wrong/unnecessary ones (recommended)",
|
|
201
|
+
"Don't answer, just fix",
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const DELAY_OPTIONS = [
|
|
205
|
+
"60 seconds",
|
|
206
|
+
"120 seconds",
|
|
207
|
+
"180 seconds (recommended)",
|
|
208
|
+
"300 seconds",
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const ITERATION_OPTIONS = [
|
|
212
|
+
"1",
|
|
213
|
+
"2",
|
|
214
|
+
"3 (recommended)",
|
|
215
|
+
"5",
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const MODEL_TIER_OPTIONS = [
|
|
219
|
+
"high — thorough reasoning, more tokens",
|
|
220
|
+
"low — fast execution, fewer tokens",
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
async function runSetupWizard(ctx: any): Promise<FixPrConfig | null> {
|
|
224
|
+
// 1. Automated reviewer
|
|
225
|
+
const reviewerChoice = await ctx.ui.select(
|
|
226
|
+
"Automated PR reviewer",
|
|
227
|
+
REVIEWER_OPTIONS,
|
|
228
|
+
{ helpText: "Select your automated reviewer, if any" },
|
|
229
|
+
);
|
|
230
|
+
if (!reviewerChoice) return null;
|
|
231
|
+
|
|
232
|
+
let reviewerType: ReviewerType = "none";
|
|
233
|
+
let triggerMethod: string | null = null;
|
|
234
|
+
|
|
235
|
+
if (reviewerChoice !== "None") {
|
|
236
|
+
reviewerType = reviewerChoice.toLowerCase().replace(/ /g, "").replace("github", "") as ReviewerType;
|
|
237
|
+
// Normalize to our type names
|
|
238
|
+
if (reviewerChoice === "CodeRabbit") reviewerType = "coderabbit";
|
|
239
|
+
else if (reviewerChoice === "GitHub Copilot") reviewerType = "copilot";
|
|
240
|
+
else if (reviewerChoice === "Gemini Code Review") reviewerType = "gemini";
|
|
241
|
+
|
|
242
|
+
const defaultTrigger = REVIEWER_DEFAULTS[reviewerChoice] || "";
|
|
243
|
+
triggerMethod = await ctx.ui.input(
|
|
244
|
+
"How to trigger re-review?",
|
|
245
|
+
defaultTrigger,
|
|
246
|
+
{ helpText: `Default for ${reviewerChoice}: ${defaultTrigger}` },
|
|
247
|
+
);
|
|
248
|
+
if (triggerMethod === undefined) return null;
|
|
249
|
+
if (!triggerMethod) triggerMethod = defaultTrigger;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 2. Comment reply policy
|
|
253
|
+
const policyChoice = await ctx.ui.select(
|
|
254
|
+
"Comment reply policy",
|
|
255
|
+
POLICY_OPTIONS,
|
|
256
|
+
{ helpText: "How should we handle replying to comments?" },
|
|
257
|
+
);
|
|
258
|
+
if (!policyChoice) return null;
|
|
259
|
+
|
|
260
|
+
let commentPolicy: CommentReplyPolicy = "answer-selective";
|
|
261
|
+
if (policyChoice.startsWith("Answer all")) commentPolicy = "answer-all";
|
|
262
|
+
else if (policyChoice.startsWith("Don't")) commentPolicy = "no-answer";
|
|
263
|
+
|
|
264
|
+
// 3. Loop timing
|
|
265
|
+
const delayChoice = await ctx.ui.select(
|
|
266
|
+
"Delay between review checks",
|
|
267
|
+
DELAY_OPTIONS,
|
|
268
|
+
{ helpText: "How long to wait for reviewer after pushing changes" },
|
|
269
|
+
);
|
|
270
|
+
if (!delayChoice) return null;
|
|
271
|
+
const delaySeconds = parseInt(delayChoice, 10);
|
|
272
|
+
|
|
273
|
+
const iterChoice = await ctx.ui.select(
|
|
274
|
+
"Max review iterations",
|
|
275
|
+
ITERATION_OPTIONS,
|
|
276
|
+
{ helpText: "Maximum fix-check-fix cycles" },
|
|
277
|
+
);
|
|
278
|
+
if (!iterChoice) return null;
|
|
279
|
+
const maxIterations = parseInt(iterChoice, 10);
|
|
280
|
+
|
|
281
|
+
// 4. Model preferences
|
|
282
|
+
const orchestratorTier = await ctx.ui.select(
|
|
283
|
+
"Orchestrator model tier (assessment & grouping)",
|
|
284
|
+
MODEL_TIER_OPTIONS,
|
|
285
|
+
{ helpText: "Higher tier = more thorough analysis" },
|
|
286
|
+
);
|
|
287
|
+
if (!orchestratorTier) return null;
|
|
288
|
+
|
|
289
|
+
const plannerTier = await ctx.ui.select(
|
|
290
|
+
"Planner model tier (fix planning)",
|
|
291
|
+
MODEL_TIER_OPTIONS,
|
|
292
|
+
{ helpText: "Higher tier = more detailed plans" },
|
|
293
|
+
);
|
|
294
|
+
if (!plannerTier) return null;
|
|
295
|
+
|
|
296
|
+
const fixerTier = await ctx.ui.select(
|
|
297
|
+
"Fixer model tier (code changes)",
|
|
298
|
+
MODEL_TIER_OPTIONS,
|
|
299
|
+
{ helpText: "Lower tier usually sufficient for execution" },
|
|
300
|
+
);
|
|
301
|
+
if (!fixerTier) return null;
|
|
302
|
+
|
|
303
|
+
const config: FixPrConfig = {
|
|
304
|
+
reviewer: { type: reviewerType, triggerMethod },
|
|
305
|
+
commentPolicy,
|
|
306
|
+
loop: { delaySeconds, maxIterations },
|
|
307
|
+
models: {
|
|
308
|
+
orchestrator: {
|
|
309
|
+
...DEFAULT_FIX_PR_CONFIG.models.orchestrator,
|
|
310
|
+
tier: orchestratorTier.startsWith("high") ? "high" : "low",
|
|
311
|
+
},
|
|
312
|
+
planner: {
|
|
313
|
+
...DEFAULT_FIX_PR_CONFIG.models.planner,
|
|
314
|
+
tier: plannerTier.startsWith("high") ? "high" : "low",
|
|
315
|
+
},
|
|
316
|
+
fixer: {
|
|
317
|
+
...DEFAULT_FIX_PR_CONFIG.models.fixer,
|
|
318
|
+
tier: fixerTier.startsWith("high") ? "high" : "low",
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return config;
|
|
324
|
+
}
|
package/src/commands/plan.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
2
|
import { loadConfig } from "../config/loader.js";
|
|
3
3
|
import { savePlan } from "../storage/plans.js";
|
|
4
|
-
import { notifySuccess, notifyInfo } from "../notifications/renderer.js";
|
|
4
|
+
import { notifySuccess, notifyInfo, notifyError } from "../notifications/renderer.js";
|
|
5
|
+
import {
|
|
6
|
+
generateVisualSessionId,
|
|
7
|
+
createSessionDir,
|
|
8
|
+
getScriptsDir,
|
|
9
|
+
parseServerInfo,
|
|
10
|
+
} from "../visual/companion.js";
|
|
11
|
+
import { buildVisualInstructions } from "../visual/prompt-instructions.js";
|
|
12
|
+
import { buildPlanningPrompt, buildQuickPlanPrompt } from "../planning/prompt-builder.js";
|
|
5
13
|
import * as fs from "node:fs";
|
|
6
14
|
import * as path from "node:path";
|
|
7
15
|
|
|
16
|
+
/** Module-level tracking for cleanup */
|
|
17
|
+
let activeSessionDir: string | null = null;
|
|
18
|
+
|
|
19
|
+
export function getActiveVisualSessionDir(): string | null {
|
|
20
|
+
return activeSessionDir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function setActiveVisualSessionDir(dir: string | null): void {
|
|
24
|
+
activeSessionDir = dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
8
27
|
export function registerPlanCommand(pi: ExtensionAPI): void {
|
|
9
28
|
pi.registerCommand("supi:plan", {
|
|
10
29
|
description: "Start collaborative planning for a feature or task",
|
|
@@ -24,39 +43,85 @@ export function registerPlanCommand(pi: ExtensionAPI): void {
|
|
|
24
43
|
const isQuick = args?.startsWith("--quick");
|
|
25
44
|
const quickDesc = isQuick ? args.replace("--quick", "").trim() : "";
|
|
26
45
|
|
|
46
|
+
// ── Visual companion consent ──────────────────────────────────
|
|
47
|
+
let visualUrl: string | null = null;
|
|
48
|
+
let visualSessionDir: string | null = null;
|
|
49
|
+
|
|
50
|
+
if (ctx.hasUI && !isQuick) {
|
|
51
|
+
const modeChoice = await ctx.ui.select(
|
|
52
|
+
"Planning mode",
|
|
53
|
+
[
|
|
54
|
+
"Terminal only",
|
|
55
|
+
"Terminal + Visual companion (opens browser)",
|
|
56
|
+
],
|
|
57
|
+
{ helpText: "Visual companion shows mockups and diagrams in a browser · Esc to cancel" },
|
|
58
|
+
);
|
|
59
|
+
if (!modeChoice) return;
|
|
60
|
+
|
|
61
|
+
if (modeChoice.startsWith("Terminal + Visual")) {
|
|
62
|
+
const sessionId = generateVisualSessionId();
|
|
63
|
+
visualSessionDir = createSessionDir(ctx.cwd, sessionId);
|
|
64
|
+
const scriptsDir = getScriptsDir();
|
|
65
|
+
|
|
66
|
+
// Install server dependencies if needed
|
|
67
|
+
const nodeModules = path.join(scriptsDir, "node_modules");
|
|
68
|
+
if (!fs.existsSync(nodeModules)) {
|
|
69
|
+
notifyInfo(ctx, "Installing visual companion dependencies...");
|
|
70
|
+
const installResult = await pi.exec("npm", ["install", "--production"], { cwd: scriptsDir });
|
|
71
|
+
if (installResult.code !== 0) {
|
|
72
|
+
notifyError(ctx, "Failed to install visual companion dependencies", installResult.stderr);
|
|
73
|
+
visualSessionDir = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (visualSessionDir) {
|
|
78
|
+
// Stop any previous visual companion
|
|
79
|
+
if (activeSessionDir) {
|
|
80
|
+
const stopScript = path.join(scriptsDir, "stop-server.sh");
|
|
81
|
+
await pi.exec("bash", [stopScript, activeSessionDir], { cwd: scriptsDir });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Start the server (pass session dir via env command since ExecOptions has no env)
|
|
85
|
+
const startScript = path.join(scriptsDir, "start-server.sh");
|
|
86
|
+
const startResult = await pi.exec("env", [
|
|
87
|
+
`SUPI_VISUAL_DIR=${visualSessionDir}`,
|
|
88
|
+
"bash",
|
|
89
|
+
startScript,
|
|
90
|
+
], { cwd: scriptsDir });
|
|
91
|
+
|
|
92
|
+
if (startResult.code === 0) {
|
|
93
|
+
const serverInfo = parseServerInfo(startResult.stdout);
|
|
94
|
+
if (serverInfo) {
|
|
95
|
+
visualUrl = serverInfo.url;
|
|
96
|
+
activeSessionDir = visualSessionDir;
|
|
97
|
+
notifyInfo(ctx, "Visual companion ready", visualUrl);
|
|
98
|
+
} else {
|
|
99
|
+
notifyError(ctx, "Visual companion started but no connection info received");
|
|
100
|
+
visualSessionDir = null;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
const errorMsg = startResult.stderr || startResult.stdout;
|
|
104
|
+
notifyError(ctx, "Failed to start visual companion", errorMsg);
|
|
105
|
+
visualSessionDir = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Build prompt ──────────────────────────────────────────────
|
|
27
112
|
let prompt: string;
|
|
28
113
|
if (isQuick && quickDesc) {
|
|
29
|
-
prompt =
|
|
30
|
-
"Generate a concise implementation plan for the following task.",
|
|
31
|
-
"Skip brainstorming — go straight to task breakdown.",
|
|
32
|
-
"",
|
|
33
|
-
`Task: ${quickDesc}`,
|
|
34
|
-
"",
|
|
35
|
-
"Format the plan as markdown with YAML frontmatter (name, created, tags).",
|
|
36
|
-
"Each task should have: name, [parallel-safe] or [sequential] annotation,",
|
|
37
|
-
"**files**, **criteria**, and **complexity** (small/medium/large).",
|
|
38
|
-
"",
|
|
39
|
-
skillContent ? "Follow these planning guidelines:\n" + skillContent : "",
|
|
40
|
-
"",
|
|
41
|
-
"After generating the plan, save it and confirm with the user.",
|
|
42
|
-
].join("\n");
|
|
114
|
+
prompt = buildQuickPlanPrompt(quickDesc, skillContent || undefined);
|
|
43
115
|
} else {
|
|
44
|
-
prompt =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"",
|
|
54
|
-
"Format the final plan as markdown with YAML frontmatter (name, created, tags).",
|
|
55
|
-
"Each task: name, [parallel-safe] or [sequential] annotation,",
|
|
56
|
-
"**files**, **criteria**, **complexity** (small/medium/large).",
|
|
57
|
-
"",
|
|
58
|
-
skillContent ? "Follow these planning guidelines:\n" + skillContent : "",
|
|
59
|
-
].join("\n");
|
|
116
|
+
prompt = buildPlanningPrompt({
|
|
117
|
+
topic: args || undefined,
|
|
118
|
+
skillContent: skillContent || undefined,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Append visual companion instructions if active
|
|
123
|
+
if (visualUrl && visualSessionDir) {
|
|
124
|
+
prompt += "\n\n" + buildVisualInstructions(visualUrl, visualSessionDir);
|
|
60
125
|
}
|
|
61
126
|
|
|
62
127
|
pi.sendMessage(
|
package/src/commands/qa.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
2
|
import { detectAndCache } from "../qa/detector.js";
|
|
3
|
-
import { buildQaRunPrompt } from "../qa/runner.js";
|
|
4
3
|
import { notifyInfo, notifyError } from "../notifications/renderer.js";
|
|
4
|
+
import { findActiveSession, findSessionWithFailures } from "../storage/qa-sessions.js";
|
|
5
|
+
import {
|
|
6
|
+
createNewSession,
|
|
7
|
+
advancePhase,
|
|
8
|
+
getFailedTests,
|
|
9
|
+
getNextPhase,
|
|
10
|
+
getPhaseStatusLine,
|
|
11
|
+
} from "../qa/session.js";
|
|
12
|
+
import { buildDiscoveryPrompt } from "../qa/phases/discovery.js";
|
|
13
|
+
import { buildMatrixPrompt } from "../qa/phases/matrix.js";
|
|
14
|
+
import { buildExecutionPrompt } from "../qa/phases/execution.js";
|
|
15
|
+
import { buildReportingPrompt } from "../qa/phases/reporting.js";
|
|
16
|
+
import type { QaPhase, QaSessionLedger } from "../types.js";
|
|
17
|
+
|
|
18
|
+
const PHASE_LABELS: Record<QaPhase, string> = {
|
|
19
|
+
discovery: "Discovery — Scan for test cases",
|
|
20
|
+
matrix: "Matrix — Build traceability matrix",
|
|
21
|
+
execution: "Execution — Run tests",
|
|
22
|
+
reporting: "Reporting — Generate summary",
|
|
23
|
+
};
|
|
5
24
|
|
|
6
25
|
export function registerQaCommand(pi: ExtensionAPI): void {
|
|
7
26
|
pi.registerCommand("supi:qa", {
|
|
8
|
-
description: "Run QA pipeline (
|
|
27
|
+
description: "Run QA pipeline with session management (discovery → matrix → execution → reporting)",
|
|
9
28
|
async handler(args, ctx) {
|
|
10
29
|
const framework = detectAndCache(ctx.cwd);
|
|
11
30
|
|
|
@@ -13,48 +32,150 @@ export function registerQaCommand(pi: ExtensionAPI): void {
|
|
|
13
32
|
notifyError(
|
|
14
33
|
ctx,
|
|
15
34
|
"No test framework detected",
|
|
16
|
-
"Configure manually
|
|
35
|
+
"Configure manually via /supi:config"
|
|
17
36
|
);
|
|
18
37
|
return;
|
|
19
38
|
}
|
|
20
39
|
|
|
21
|
-
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
// ── Step 1: Session selection ──────────────────────────────────
|
|
41
|
+
let ledger: QaSessionLedger | null = null;
|
|
42
|
+
|
|
43
|
+
const activeSession = findActiveSession(ctx.cwd);
|
|
44
|
+
const failedSession = findSessionWithFailures(ctx.cwd);
|
|
45
|
+
|
|
46
|
+
if (ctx.hasUI && !args?.trim()) {
|
|
47
|
+
const sessionOptions: string[] = [];
|
|
48
|
+
|
|
49
|
+
if (failedSession) {
|
|
50
|
+
const failCount = failedSession.results.filter((r) => r.status === "fail").length;
|
|
51
|
+
sessionOptions.push(`Resume ${failedSession.id} (${failCount} failed test${failCount !== 1 ? "s" : ""})`);
|
|
52
|
+
} else if (activeSession) {
|
|
53
|
+
const next = getNextPhase(activeSession);
|
|
54
|
+
sessionOptions.push(`Resume ${activeSession.id} (${next ?? "all phases done"} pending)`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sessionOptions.push("Start new session");
|
|
58
|
+
|
|
59
|
+
if (sessionOptions.length > 1) {
|
|
60
|
+
const choice = await ctx.ui.select(
|
|
61
|
+
"QA Session",
|
|
62
|
+
sessionOptions,
|
|
63
|
+
{ helpText: "Select session · Esc to cancel" },
|
|
64
|
+
);
|
|
65
|
+
if (!choice) return;
|
|
66
|
+
|
|
67
|
+
if (choice.startsWith("Resume")) {
|
|
68
|
+
ledger = failedSession ?? activeSession;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
37
71
|
}
|
|
38
72
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
// Create new session if none selected
|
|
74
|
+
if (!ledger) {
|
|
75
|
+
ledger = createNewSession(ctx.cwd, framework.name);
|
|
76
|
+
notifyInfo(ctx, "QA session created", ledger.id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Step 2: Phase selection ────────────────────────────────────
|
|
80
|
+
type PhaseAction =
|
|
81
|
+
| { type: "run-phase"; phase: QaPhase }
|
|
82
|
+
| { type: "rerun-failed" };
|
|
83
|
+
|
|
84
|
+
let action: PhaseAction | null = null;
|
|
85
|
+
const nextPhase = getNextPhase(ledger);
|
|
86
|
+
const failedTests = getFailedTests(ledger);
|
|
87
|
+
|
|
88
|
+
if (ctx.hasUI && !args?.trim()) {
|
|
89
|
+
const phaseOptions: string[] = [];
|
|
90
|
+
|
|
91
|
+
// Offer re-run failed if there are failures
|
|
92
|
+
if (failedTests.length > 0) {
|
|
93
|
+
phaseOptions.push(`Re-run ${failedTests.length} failed test${failedTests.length !== 1 ? "s" : ""} only`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Offer starting from next pending phase
|
|
97
|
+
if (nextPhase) {
|
|
98
|
+
phaseOptions.push(PHASE_LABELS[nextPhase]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (phaseOptions.length > 1) {
|
|
102
|
+
const statusLine = getPhaseStatusLine(ledger);
|
|
103
|
+
const choice = await ctx.ui.select(
|
|
104
|
+
`QA Phase · ${statusLine}`,
|
|
105
|
+
phaseOptions,
|
|
106
|
+
{ helpText: "Select action · Esc to cancel" },
|
|
107
|
+
);
|
|
108
|
+
if (!choice) return;
|
|
109
|
+
|
|
110
|
+
if (choice.startsWith("Re-run")) {
|
|
111
|
+
action = { type: "rerun-failed" };
|
|
112
|
+
} else {
|
|
113
|
+
// Extract phase from label
|
|
114
|
+
const selectedPhase = (Object.entries(PHASE_LABELS) as [QaPhase, string][])
|
|
115
|
+
.find(([, label]) => label === choice)?.[0];
|
|
116
|
+
if (selectedPhase) {
|
|
117
|
+
action = { type: "run-phase", phase: selectedPhase };
|
|
118
|
+
}
|
|
44
119
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
120
|
+
} else if (nextPhase) {
|
|
121
|
+
// Only one option — just run the next phase
|
|
122
|
+
action = { type: "run-phase", phase: nextPhase };
|
|
47
123
|
}
|
|
124
|
+
} else if (nextPhase) {
|
|
125
|
+
action = { type: "run-phase", phase: nextPhase };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!action) {
|
|
129
|
+
notifyInfo(ctx, "QA pipeline complete", getPhaseStatusLine(ledger));
|
|
130
|
+
return;
|
|
48
131
|
}
|
|
49
132
|
|
|
50
|
-
|
|
133
|
+
// ── Step 3: Execute ────────────────────────────────────────────
|
|
134
|
+
let prompt: string;
|
|
135
|
+
|
|
136
|
+
if (action.type === "rerun-failed") {
|
|
137
|
+
ledger = advancePhase(ctx.cwd, ledger, "execution", "running");
|
|
138
|
+
prompt = buildExecutionPrompt(ledger, { failedOnly: true, failedTests });
|
|
139
|
+
notifyInfo(ctx, "QA re-running failed tests", `${failedTests.length} test(s)`);
|
|
140
|
+
} else {
|
|
141
|
+
const phase = action.phase;
|
|
142
|
+
ledger = advancePhase(ctx.cwd, ledger, phase, "running");
|
|
143
|
+
|
|
144
|
+
switch (phase) {
|
|
145
|
+
case "discovery":
|
|
146
|
+
prompt = buildDiscoveryPrompt(framework, ctx.cwd);
|
|
147
|
+
break;
|
|
148
|
+
case "matrix":
|
|
149
|
+
prompt = buildMatrixPrompt(ledger);
|
|
150
|
+
break;
|
|
151
|
+
case "execution":
|
|
152
|
+
prompt = buildExecutionPrompt(ledger);
|
|
153
|
+
break;
|
|
154
|
+
case "reporting":
|
|
155
|
+
prompt = buildReportingPrompt(ledger);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
notifyInfo(ctx, `QA phase: ${phase}`, `session: ${ledger.id}`);
|
|
160
|
+
}
|
|
51
161
|
|
|
52
|
-
|
|
162
|
+
// Include session context for the sub-agent
|
|
163
|
+
const sessionContext = [
|
|
164
|
+
`\n\n## QA Session Context`,
|
|
165
|
+
``,
|
|
166
|
+
`Session ID: ${ledger.id}`,
|
|
167
|
+
`Session ledger path: .omp/supipowers/qa-sessions/${ledger.id}/ledger.json`,
|
|
168
|
+
``,
|
|
169
|
+
`Current ledger state:`,
|
|
170
|
+
"```json",
|
|
171
|
+
JSON.stringify(ledger, null, 2),
|
|
172
|
+
"```",
|
|
173
|
+
].join("\n");
|
|
53
174
|
|
|
54
175
|
pi.sendMessage(
|
|
55
176
|
{
|
|
56
177
|
customType: "supi-qa",
|
|
57
|
-
content: [{ type: "text", text: prompt }],
|
|
178
|
+
content: [{ type: "text", text: prompt + sessionContext }],
|
|
58
179
|
display: "none",
|
|
59
180
|
},
|
|
60
181
|
{ deliverAs: "steer" }
|