supipowers 0.3.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "OMP-native workflow extension inspired by Superpowers.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: fix-pr
3
+ description: Critically assess PR review comments — verify, investigate ripple effects, then fix or reject with evidence
4
+ ---
5
+
6
+ # PR Review Comment Assessment
7
+
8
+ ## Core Principle
9
+
10
+ Review comments are suggestions to evaluate, not orders to follow.
11
+ Assess each one critically before acting. The reviewer may lack context you have.
12
+
13
+ ## Assessment Framework
14
+
15
+ ### For Each Comment, Answer:
16
+
17
+ 1. **Is this valid?** Read the actual code being commented on. Does the concern apply?
18
+ 2. **Is this important?** Bug fix vs style preference vs premature optimization.
19
+ 3. **What breaks if we change this?** Trace callers, check tests, find ripple effects.
20
+ 4. **Does the reviewer have full context?** They often review diffs, not the full picture.
21
+ 5. **Is this YAGNI?** "You should also handle X" — but does X actually occur?
22
+
23
+ ### Verdict Categories
24
+
25
+ - **ACCEPT**: Valid concern, should fix. Evidence: the code has the problem described.
26
+ - **REJECT**: Invalid, unnecessary, or would cause harm. Evidence: why this doesn't apply.
27
+ - **INVESTIGATE**: Need to check more before deciding. List what to check.
28
+
29
+ ### Investigation Protocol
30
+
31
+ When INVESTIGATE:
32
+ 1. Read the file(s) mentioned in full (not just the diff)
33
+ 2. Search for usages of the symbol/pattern being discussed
34
+ 3. Check test coverage for the area
35
+ 4. Look at git blame — why is the code written this way?
36
+ 5. Then decide ACCEPT or REJECT with evidence
37
+
38
+ ## Ripple Effect Analysis
39
+
40
+ Before accepting any change:
41
+ 1. **Who calls this?** Search for usages of the function/method/class
42
+ 2. **Who depends on this behavior?** Check tests that assert current behavior
43
+ 3. **What imports this?** Follow the dependency graph
44
+ 4. **Is this a public API?** Changes to public interfaces affect consumers
45
+
46
+ If ripple effects are significant, note them in the plan so the fixer handles them.
47
+
48
+ ## Grouping Strategy
49
+
50
+ Group comments that:
51
+ - Touch the same file
52
+ - Touch tightly coupled files (caller/callee, type/implementation)
53
+ - Relate to the same logical concern (e.g., "error handling in module X")
54
+
55
+ Keep separate:
56
+ - Comments on unrelated files/areas
57
+ - Cosmetic vs functional changes
58
+ - Independent features or concerns
59
+
60
+ ## Comment Reply Guidelines
61
+
62
+ ### For ACCEPT:
63
+ - "Fixed. [description of change]."
64
+ - "Fixed in [file]. Also updated [related file] to maintain consistency."
65
+
66
+ ### For REJECT:
67
+ - "Investigated — [reason this doesn't apply]. The current implementation [explanation]."
68
+ - "This is intentional: [reason]. Changing it would [consequence]."
69
+
70
+ ### For grouped fixes:
71
+ - "Addressed these comments together in [commit]. Changes: [bullet list]."
72
+
73
+ **Never use performative agreement.** No "Great catch!", "You're absolutely right!", etc.
74
+ Technical acknowledgment only.
75
+
76
+ ## Common Reviewer Mistakes to Watch For
77
+
78
+ | Pattern | Reality |
79
+ |---------|---------|
80
+ | Suggesting abstraction for code used once | YAGNI — one usage doesn't need a helper |
81
+ | Requesting error handling for impossible states | Trust internal code; only validate at boundaries |
82
+ | Style preferences disguised as correctness | If it works and is readable, style is preference |
83
+ | Suggesting patterns from a different language | Follow THIS codebase's patterns |
84
+ | Not seeing the full file (diff-only context) | They may miss why code is structured this way |
85
+ | "This could be a security issue" without specifics | Ask for the specific attack vector |
86
+ | "Add tests for X" when X is already tested | Check before accepting |
87
+
88
+ ## Decision Record
89
+
90
+ For each comment, record:
91
+ ```
92
+ Comment #ID by @user on file:line
93
+ Verdict: ACCEPT | REJECT | INVESTIGATE
94
+ Reasoning: [1-2 sentences]
95
+ Ripple effects: [list or "none"]
96
+ Group: [group-id]
97
+ ```
98
+
99
+ This record serves as the basis for reply content and fix planning.
@@ -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
+ }
@@ -16,6 +16,7 @@ export function handleSupi(pi: ExtensionAPI, ctx: ExtensionContext): void {
16
16
  "/supi:run — Execute a plan with sub-agents",
17
17
  "/supi:review — Run quality gates",
18
18
  "/supi:qa — Run QA pipeline",
19
+ "/supi:fix-pr — Fix PR review comments",
19
20
  "/supi:release — Release automation",
20
21
  "/supi:config — Manage configuration",
21
22
  "/supi:status — Check running tasks",
@@ -0,0 +1,36 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { FixPrConfig } from "./types.js";
4
+
5
+ const CONFIG_FILENAME = "fix-pr.json";
6
+
7
+ function getConfigPath(cwd: string): string {
8
+ return path.join(cwd, ".omp", "supipowers", CONFIG_FILENAME);
9
+ }
10
+
11
+ export const DEFAULT_FIX_PR_CONFIG: FixPrConfig = {
12
+ reviewer: { type: "none", triggerMethod: null },
13
+ commentPolicy: "answer-selective",
14
+ loop: { delaySeconds: 180, maxIterations: 3 },
15
+ models: {
16
+ orchestrator: { provider: "anthropic", model: "claude-opus-4-6", tier: "high" },
17
+ planner: { provider: "anthropic", model: "claude-opus-4-6", tier: "high" },
18
+ fixer: { provider: "anthropic", model: "claude-sonnet-4-6", tier: "low" },
19
+ },
20
+ };
21
+
22
+ export function loadFixPrConfig(cwd: string): FixPrConfig | null {
23
+ const configPath = getConfigPath(cwd);
24
+ if (!fs.existsSync(configPath)) return null;
25
+ try {
26
+ return JSON.parse(fs.readFileSync(configPath, "utf-8")) as FixPrConfig;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export function saveFixPrConfig(cwd: string, config: FixPrConfig): void {
33
+ const configPath = getConfigPath(cwd);
34
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
35
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
36
+ }
@@ -0,0 +1,201 @@
1
+ import type { FixPrConfig } from "./types.js";
2
+ import { buildReceivingReviewInstructions } from "../discipline/receiving-review.js";
3
+
4
+ export interface FixPrPromptOptions {
5
+ prNumber: number;
6
+ repo: string;
7
+ comments: string;
8
+ sessionDir: string;
9
+ scriptsDir: string;
10
+ config: FixPrConfig;
11
+ iteration: number;
12
+ skillContent: string;
13
+ }
14
+
15
+ function buildReplyInstructions(config: FixPrConfig): string {
16
+ const { commentPolicy, repo, } = config;
17
+ const replyCmd = `gh api repos/REPO/pulls/PR/comments/COMMENT_ID/replies -f body="..."`;
18
+
19
+ switch (commentPolicy) {
20
+ case "no-answer":
21
+ return [
22
+ "### Comment Replies",
23
+ "",
24
+ "Policy: **Do not reply** to any comments. Focus only on fixing the code.",
25
+ "Do not post any replies via gh api.",
26
+ ].join("\n");
27
+ case "answer-all":
28
+ return [
29
+ "### Comment Replies",
30
+ "",
31
+ "Policy: **Answer all** comments — both accepted and rejected.",
32
+ "For each comment, post a reply explaining what was done or why it was rejected.",
33
+ `Use: \`${replyCmd}\``,
34
+ "Keep replies factual and technical. No performative agreement.",
35
+ ].join("\n");
36
+ case "answer-selective":
37
+ return [
38
+ "### Comment Replies",
39
+ "",
40
+ "Policy: **Answer selectively** — only reply to comments you reject or where clarification adds value.",
41
+ "For ACCEPT: fix silently (the code change speaks for itself).",
42
+ "For REJECT: explain why with technical reasoning.",
43
+ `Use: \`${replyCmd}\``,
44
+ "Keep replies factual. No performative agreement.",
45
+ ].join("\n");
46
+ }
47
+ }
48
+
49
+ export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): string {
50
+ const { prNumber, repo, comments, sessionDir, scriptsDir, config, iteration, skillContent } = options;
51
+ const { loop, models, reviewer } = config;
52
+ const maxIter = loop.maxIterations;
53
+ const delay = loop.delaySeconds;
54
+
55
+ const sections: string[] = [
56
+ "# PR Review Fix Orchestration",
57
+ "",
58
+ `You are the orchestrator for fixing PR #${prNumber} on \`${repo}\`.`,
59
+ "",
60
+ "## Session Context",
61
+ "",
62
+ `- Session dir: \`${sessionDir}\``,
63
+ `- Iteration: ${iteration} of ${maxIter}`,
64
+ `- Comment reply policy: ${config.commentPolicy}`,
65
+ `- Reviewer: ${reviewer.type}${reviewer.triggerMethod ? ` (trigger: ${reviewer.triggerMethod})` : ""}`,
66
+ "",
67
+ "## Review Comments to Process",
68
+ "",
69
+ "Each line is a JSON object with comment data:",
70
+ "",
71
+ "```jsonl",
72
+ comments,
73
+ "```",
74
+ "",
75
+ ];
76
+
77
+ // Embedded skill
78
+ if (skillContent) {
79
+ sections.push(
80
+ "## Assessment Methodology",
81
+ "",
82
+ skillContent,
83
+ "",
84
+ );
85
+ }
86
+
87
+ // Receiving review discipline
88
+ sections.push(
89
+ "## Review Discipline",
90
+ "",
91
+ buildReceivingReviewInstructions(),
92
+ "",
93
+ );
94
+
95
+ // Step 1: Assess
96
+ sections.push(
97
+ "## Step 1: Assess Each Comment",
98
+ "",
99
+ "For each comment:",
100
+ "1. Read the actual code at the file and line referenced",
101
+ "2. Determine the verdict: **ACCEPT** / **REJECT** / **INVESTIGATE**",
102
+ "3. Check ripple effects — who calls this, what tests cover it",
103
+ "4. YAGNI check — does the reviewer's suggestion address a real problem?",
104
+ "",
105
+ "Record your assessment:",
106
+ "```",
107
+ "Comment #ID by @user on file:line",
108
+ "Verdict: ACCEPT | REJECT | INVESTIGATE",
109
+ "Reasoning: [1-2 sentences]",
110
+ "Ripple effects: [list or none]",
111
+ "Group: [group-id]",
112
+ "```",
113
+ "",
114
+ );
115
+
116
+ // Step 2: Group
117
+ sections.push(
118
+ "## Step 2: Group Comments",
119
+ "",
120
+ "Group accepted comments for parallel execution:",
121
+ "- Same file or tightly coupled files → same group",
122
+ "- Independent files/areas → separate groups",
123
+ "- Cosmetic vs functional → separate groups",
124
+ "",
125
+ );
126
+
127
+ // Step 3: Plan
128
+ sections.push(
129
+ "## Step 3: Plan Each Group",
130
+ "",
131
+ "For each group, create a fix plan:",
132
+ "- What changes are needed and why",
133
+ "- Which files to modify",
134
+ "- Expected ripple effects and how to handle them",
135
+ "- How to verify the fix (which tests to run)",
136
+ "",
137
+ );
138
+
139
+ // Step 4: Execute
140
+ sections.push(
141
+ "## Step 4: Execute Fixes",
142
+ "",
143
+ "For each group:",
144
+ "1. Make the code changes",
145
+ "2. Run relevant tests to verify",
146
+ "3. If tests fail, fix before moving on",
147
+ "",
148
+ );
149
+
150
+ // Step 5: Reply
151
+ sections.push(buildReplyInstructions(config), "");
152
+
153
+ // Step 6: Push and loop
154
+ sections.push(
155
+ "## Step 6: Push and Check for New Comments",
156
+ "",
157
+ '1. Stage and commit: `git add -A && git commit -m "fix: address PR review comments (iteration ' + iteration + ')"`',
158
+ "2. Push: `git push`",
159
+ );
160
+
161
+ if (reviewer.type !== "none" && reviewer.triggerMethod) {
162
+ sections.push(
163
+ `3. Trigger re-review: \`bash ${scriptsDir}/trigger-review.sh "${repo}" ${prNumber} "${reviewer.type}" "${reviewer.triggerMethod}"\``,
164
+ );
165
+ }
166
+
167
+ sections.push(
168
+ `${reviewer.type !== "none" ? "4" : "3"}. Run the check script:`,
169
+ "```bash",
170
+ `bash ${scriptsDir}/wait-and-check.sh "${sessionDir}" ${delay} ${iteration + 1} "${repo}" ${prNumber}`,
171
+ "```",
172
+ `${reviewer.type !== "none" ? "5" : "4"}. Read the last line of output:`,
173
+ ` - If \`hasNewComments: true\` and iteration < ${maxIter}: process the new comments (go back to Step 1)`,
174
+ ` - If \`hasNewComments: false\` or iteration >= ${maxIter}: report done`,
175
+ "",
176
+ );
177
+
178
+ // Script paths reference
179
+ sections.push(
180
+ "## Script Paths",
181
+ "",
182
+ `- fetch-pr-comments.sh: \`${scriptsDir}/fetch-pr-comments.sh\``,
183
+ `- diff-comments.sh: \`${scriptsDir}/diff-comments.sh\``,
184
+ `- trigger-review.sh: \`${scriptsDir}/trigger-review.sh\``,
185
+ `- wait-and-check.sh: \`${scriptsDir}/wait-and-check.sh\``,
186
+ "",
187
+ );
188
+
189
+ // Model guidance
190
+ sections.push(
191
+ "## Model Guidance",
192
+ "",
193
+ `- **Orchestrator** (assessment, grouping): ${models.orchestrator.model} (${models.orchestrator.tier} tier) — thorough analysis`,
194
+ `- **Planner** (fix planning): ${models.planner.model} (${models.planner.tier} tier) — detailed planning`,
195
+ `- **Fixer** (code changes): ${models.fixer.model} (${models.fixer.tier} tier) — focused execution`,
196
+ "",
197
+ "These indicate the expected reasoning depth for each phase of work.",
198
+ );
199
+
200
+ return sections.join("\n");
201
+ }
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # Compares two JSONL comment snapshots, outputs only new/changed comments
3
+ # Usage: diff-comments.sh <prev_snapshot> <new_snapshot>
4
+ # Exit 0 if new comments found, exit 1 if identical
5
+ set -euo pipefail
6
+
7
+ PREV="$1"
8
+ NEW="$2"
9
+
10
+ # If no previous snapshot, all comments are new
11
+ if [[ ! -f "$PREV" ]]; then
12
+ cat "$NEW"
13
+ exit 0
14
+ fi
15
+
16
+ # Build fingerprint: id + updatedAt for each comment
17
+ prev_fingerprints=$(jq -r '[.id, .updatedAt] | @tsv' "$PREV" 2>/dev/null | sort)
18
+ new_fingerprints=$(jq -r '[.id, .updatedAt] | @tsv' "$NEW" 2>/dev/null | sort)
19
+
20
+ # Find IDs that are new or changed
21
+ new_ids=$(comm -13 <(echo "$prev_fingerprints") <(echo "$new_fingerprints") | cut -f1)
22
+
23
+ if [[ -z "$new_ids" ]]; then
24
+ exit 1
25
+ fi
26
+
27
+ # Output the full comment objects for new/changed IDs
28
+ while IFS= read -r id; do
29
+ [[ -z "$id" ]] && continue
30
+ jq -c "select(.id == $id)" "$NEW"
31
+ done <<< "$new_ids"
32
+
33
+ exit 0
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+ # Fetches all review comments for a PR, outputs JSONL
3
+ # Usage: fetch-pr-comments.sh <owner/repo> <pr_number> <output_file>
4
+ set -euo pipefail
5
+
6
+ REPO="$1"
7
+ PR="$2"
8
+ OUTPUT="$3"
9
+
10
+ # Ensure output directory exists
11
+ mkdir -p "$(dirname "$OUTPUT")"
12
+
13
+ # Fetch inline review comments (code-level)
14
+ gh api --paginate "repos/${REPO}/pulls/${PR}/comments" \
15
+ --jq '.[] | {id, path, line: .line, body, user: .user.login, createdAt: .created_at, updatedAt: .updated_at, inReplyToId: .in_reply_to_id, diffHunk: .diff_hunk, state: "COMMENTED"}' \
16
+ > "$OUTPUT" 2>/dev/null || true
17
+
18
+ # Fetch review-level comments (top-level reviews with body text)
19
+ gh api --paginate "repos/${REPO}/pulls/${PR}/reviews" \
20
+ --jq '.[] | select(.body != null and .body != "") | {id, path: null, line: null, body, user: .user.login, createdAt: .submitted_at, updatedAt: .submitted_at, inReplyToId: null, diffHunk: null, state}' \
21
+ >> "$OUTPUT" 2>/dev/null || true
22
+
23
+ # Output summary to stderr for caller
24
+ TOTAL=$(wc -l < "$OUTPUT" | tr -d ' ')
25
+ echo "{\"total\": ${TOTAL}}" >&2
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ # Triggers automated reviewer to re-review a PR
3
+ # Usage: trigger-review.sh <owner/repo> <pr_number> <reviewer_type> <trigger_method>
4
+ set -euo pipefail
5
+
6
+ REPO="$1"
7
+ PR="$2"
8
+ REVIEWER="$3"
9
+ METHOD="${4:-}"
10
+
11
+ case "$REVIEWER" in
12
+ coderabbit)
13
+ gh api "repos/${REPO}/issues/${PR}/comments" -f body="$METHOD" >/dev/null 2>&1
14
+ echo '{"triggered": true, "reviewer": "coderabbit"}'
15
+ ;;
16
+ copilot)
17
+ if [[ -n "$METHOD" ]]; then
18
+ gh api "repos/${REPO}/issues/${PR}/comments" -f body="$METHOD" >/dev/null 2>&1
19
+ else
20
+ gh api "repos/${REPO}/pulls/${PR}/requested_reviewers" \
21
+ --method POST -f "reviewers[]=copilot" >/dev/null 2>&1 || true
22
+ fi
23
+ echo '{"triggered": true, "reviewer": "copilot"}'
24
+ ;;
25
+ gemini)
26
+ gh api "repos/${REPO}/issues/${PR}/comments" -f body="$METHOD" >/dev/null 2>&1
27
+ echo '{"triggered": true, "reviewer": "gemini"}'
28
+ ;;
29
+ none)
30
+ echo '{"triggered": false, "reviewer": "none"}'
31
+ ;;
32
+ *)
33
+ echo '{"triggered": false, "error": "unknown reviewer type: '"$REVIEWER"'"}'
34
+ exit 1
35
+ ;;
36
+ esac
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # Waits for delay, fetches new PR comments, diffs against previous snapshot
3
+ # Usage: wait-and-check.sh <session_dir> <delay_seconds> <iteration> <owner/repo> <pr_number>
4
+ # Output: new comment lines + JSON summary on last line
5
+ set -euo pipefail
6
+
7
+ SESSION_DIR="$1"
8
+ DELAY="$2"
9
+ ITERATION="$3"
10
+ REPO="$4"
11
+ PR="$5"
12
+
13
+ SNAPSHOTS_DIR="${SESSION_DIR}/snapshots"
14
+ PREV_ITERATION=$((ITERATION - 1))
15
+ PREV_SNAPSHOT="${SNAPSHOTS_DIR}/comments-${PREV_ITERATION}.jsonl"
16
+ NEW_SNAPSHOT="${SNAPSHOTS_DIR}/comments-${ITERATION}.jsonl"
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
+
20
+ # Wait for reviewer to process
21
+ echo "Waiting ${DELAY}s for reviewer to process changes..." >&2
22
+ sleep "$DELAY"
23
+
24
+ # Fetch new comments
25
+ echo "Fetching PR comments (iteration ${ITERATION})..." >&2
26
+ bash "${SCRIPT_DIR}/fetch-pr-comments.sh" "$REPO" "$PR" "$NEW_SNAPSHOT"
27
+
28
+ # Diff against previous
29
+ DIFF_OUTPUT=$(bash "${SCRIPT_DIR}/diff-comments.sh" "$PREV_SNAPSHOT" "$NEW_SNAPSHOT" 2>/dev/null) || true
30
+
31
+ if [[ -n "$DIFF_OUTPUT" ]]; then
32
+ DIFF_COUNT=$(echo "$DIFF_OUTPUT" | wc -l | tr -d ' ')
33
+ echo "$DIFF_OUTPUT"
34
+ echo "{\"hasNewComments\": true, \"count\": ${DIFF_COUNT}, \"iteration\": ${ITERATION}}"
35
+ else
36
+ echo "{\"hasNewComments\": false, \"count\": 0, \"iteration\": ${ITERATION}}"
37
+ fi
@@ -0,0 +1,71 @@
1
+ /** Supported automated PR reviewers */
2
+ export type ReviewerType = "coderabbit" | "copilot" | "gemini" | "none";
3
+
4
+ /** How to handle comment replies */
5
+ export type CommentReplyPolicy = "answer-all" | "answer-selective" | "no-answer";
6
+
7
+ /** Model preference for a specific role */
8
+ export interface ModelPref {
9
+ provider: string;
10
+ model: string;
11
+ tier: "low" | "high";
12
+ }
13
+
14
+ /** Per-repo fix-pr configuration */
15
+ export interface FixPrConfig {
16
+ reviewer: {
17
+ type: ReviewerType;
18
+ triggerMethod: string | null;
19
+ };
20
+ commentPolicy: CommentReplyPolicy;
21
+ loop: {
22
+ delaySeconds: number;
23
+ maxIterations: number;
24
+ };
25
+ models: {
26
+ orchestrator: ModelPref;
27
+ planner: ModelPref;
28
+ fixer: ModelPref;
29
+ };
30
+ }
31
+
32
+ /** A PR review comment from GitHub API */
33
+ export interface PrComment {
34
+ id: number;
35
+ path: string | null;
36
+ line: number | null;
37
+ body: string;
38
+ user: string;
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ inReplyToId: number | null;
42
+ diffHunk: string | null;
43
+ state: string;
44
+ }
45
+
46
+ /** Assessment verdict for a single comment */
47
+ export type CommentVerdict = "accept" | "reject" | "investigate";
48
+
49
+ /** A group of related comments to fix together */
50
+ export interface FixGroup {
51
+ id: string;
52
+ commentIds: number[];
53
+ files: string[];
54
+ description: string;
55
+ }
56
+
57
+ /** Session status */
58
+ export type FixPrSessionStatus = "running" | "completed" | "failed";
59
+
60
+ /** Session ledger for a fix-pr run */
61
+ export interface FixPrSessionLedger {
62
+ id: string;
63
+ createdAt: string;
64
+ updatedAt: string;
65
+ prNumber: number;
66
+ repo: string;
67
+ status: FixPrSessionStatus;
68
+ iteration: number;
69
+ config: FixPrConfig;
70
+ commentsProcessed: number[];
71
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { registerReviewCommand } from "./commands/review.js";
12
12
  import { registerQaCommand } from "./commands/qa.js";
13
13
  import { registerReleaseCommand } from "./commands/release.js";
14
14
  import { registerUpdateCommand, handleUpdate } from "./commands/update.js";
15
+ import { registerFixPrCommand } from "./commands/fix-pr.js";
15
16
 
16
17
  // TUI-only commands — intercepted at the input level to prevent
17
18
  // message submission and "Working..." indicator
@@ -43,6 +44,7 @@ export default function supipowers(pi: ExtensionAPI): void {
43
44
  registerQaCommand(pi);
44
45
  registerReleaseCommand(pi);
45
46
  registerUpdateCommand(pi);
47
+ registerFixPrCommand(pi);
46
48
 
47
49
  // Intercept TUI-only commands at the input level — this runs BEFORE
48
50
  // message submission, so no chat message appears and no "Working..." indicator
@@ -0,0 +1,59 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { FixPrSessionLedger } from "../fix-pr/types.js";
4
+
5
+ const SESSIONS_DIR = "fix-pr-sessions";
6
+
7
+ function getBaseDir(cwd: string): string {
8
+ return path.join(cwd, ".omp", "supipowers", SESSIONS_DIR);
9
+ }
10
+
11
+ export function getSessionDir(cwd: string, sessionId: string): string {
12
+ return path.join(getBaseDir(cwd), sessionId);
13
+ }
14
+
15
+ export function generateFixPrSessionId(): string {
16
+ const now = new Date();
17
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
18
+ const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
19
+ const rand = Math.random().toString(36).slice(2, 6);
20
+ return `fpr-${date}-${time}-${rand}`;
21
+ }
22
+
23
+ export function createFixPrSession(cwd: string, ledger: FixPrSessionLedger): void {
24
+ const sessionDir = getSessionDir(cwd, ledger.id);
25
+ fs.mkdirSync(path.join(sessionDir, "snapshots"), { recursive: true });
26
+ fs.writeFileSync(path.join(sessionDir, "ledger.json"), JSON.stringify(ledger, null, 2));
27
+ }
28
+
29
+ export function loadFixPrSession(cwd: string, sessionId: string): FixPrSessionLedger | null {
30
+ const ledgerPath = path.join(getSessionDir(cwd, sessionId), "ledger.json");
31
+ if (!fs.existsSync(ledgerPath)) return null;
32
+ try {
33
+ return JSON.parse(fs.readFileSync(ledgerPath, "utf-8")) as FixPrSessionLedger;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function updateFixPrSession(cwd: string, ledger: FixPrSessionLedger): void {
40
+ const ledgerPath = path.join(getSessionDir(cwd, ledger.id), "ledger.json");
41
+ ledger.updatedAt = new Date().toISOString();
42
+ fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2));
43
+ }
44
+
45
+ export function findActiveFixPrSession(cwd: string): FixPrSessionLedger | null {
46
+ const baseDir = getBaseDir(cwd);
47
+ if (!fs.existsSync(baseDir)) return null;
48
+
49
+ const dirs = fs.readdirSync(baseDir)
50
+ .filter((d) => d.startsWith("fpr-"))
51
+ .sort()
52
+ .reverse();
53
+
54
+ for (const dir of dirs) {
55
+ const ledger = loadFixPrSession(cwd, dir);
56
+ if (ledger && ledger.status === "running") return ledger;
57
+ }
58
+ return null;
59
+ }