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 +1 -1
- package/skills/fix-pr/SKILL.md +99 -0
- package/src/commands/fix-pr.ts +324 -0
- package/src/commands/supi.ts +1 -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/index.ts +2 -0
- package/src/storage/fix-pr-sessions.ts +59 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/supi.ts
CHANGED
|
@@ -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
|
+
}
|