supipowers 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/fix-pr/SKILL.md +99 -0
  3. package/skills/qa-strategy/SKILL.md +103 -21
  4. package/src/commands/fix-pr.ts +324 -0
  5. package/src/commands/qa.ts +232 -148
  6. package/src/commands/supi.ts +2 -1
  7. package/src/config/defaults.ts +1 -0
  8. package/src/config/schema.ts +1 -0
  9. package/src/fix-pr/config.ts +36 -0
  10. package/src/fix-pr/prompt-builder.ts +201 -0
  11. package/src/fix-pr/scripts/diff-comments.sh +33 -0
  12. package/src/fix-pr/scripts/fetch-pr-comments.sh +25 -0
  13. package/src/fix-pr/scripts/trigger-review.sh +36 -0
  14. package/src/fix-pr/scripts/wait-and-check.sh +37 -0
  15. package/src/fix-pr/types.ts +71 -0
  16. package/src/index.ts +2 -0
  17. package/src/qa/config.ts +43 -0
  18. package/src/qa/matrix.ts +84 -0
  19. package/src/qa/prompt-builder.ts +212 -0
  20. package/src/qa/scripts/detect-app-type.sh +68 -0
  21. package/src/qa/scripts/discover-routes.sh +143 -0
  22. package/src/qa/scripts/ensure-playwright.sh +38 -0
  23. package/src/qa/scripts/run-e2e-tests.sh +99 -0
  24. package/src/qa/scripts/start-dev-server.sh +46 -0
  25. package/src/qa/scripts/stop-dev-server.sh +36 -0
  26. package/src/qa/session.ts +39 -55
  27. package/src/qa/types.ts +97 -0
  28. package/src/storage/fix-pr-sessions.ts +59 -0
  29. package/src/storage/qa-sessions.ts +9 -9
  30. package/src/types.ts +1 -70
  31. package/src/qa/detector.ts +0 -61
  32. package/src/qa/phases/discovery.ts +0 -34
  33. package/src/qa/phases/execution.ts +0 -65
  34. package/src/qa/phases/matrix.ts +0 -41
  35. package/src/qa/phases/reporting.ts +0 -71
  36. package/src/qa/report.ts +0 -22
  37. package/src/qa/runner.ts +0 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "0.3.0",
3
+ "version": "0.5.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.
@@ -1,32 +1,114 @@
1
1
  ---
2
2
  name: qa-strategy
3
- description: QA test planning for comprehensive coverage
3
+ description: E2E product testing strategy using Playwright — flow-based, autonomous, close to human interaction
4
4
  ---
5
5
 
6
- # QA Strategy Skill
6
+ # E2E Product Testing Strategy
7
7
 
8
- ## Test Pyramid
8
+ ## Core Principle
9
9
 
10
- 1. **Unit tests**: Fast, isolated, cover individual functions
11
- 2. **Integration tests**: Test component interactions
12
- 3. **E2E tests**: Test user-facing flows end-to-end
10
+ Test the product the way a user uses it. Every test simulates a real user flow — navigating, clicking, filling forms, waiting for responses. If a human wouldn't do it, don't test it here.
13
11
 
14
- ## When to Write What
12
+ **This is NOT for unit or integration tests.** This pipeline tests complete user journeys through the running application.
15
13
 
16
- - New function → unit test
17
- - New API endpoint → integration test
18
- - New user flow → E2E test
19
- - Bug fix → regression test at the appropriate level
14
+ ## Flow Discovery
20
15
 
21
- ## Coverage Priorities
16
+ Before writing tests, understand what the product does:
22
17
 
23
- Focus testing effort on:
24
- 1. Business logic (highest value)
25
- 2. Error handling paths
26
- 3. Edge cases in input validation
27
- 4. Integration points (API boundaries, DB queries)
18
+ 1. **Scan routes and pages** — every URL a user can visit is a potential flow entry point
19
+ 2. **Identify forms** login, signup, search, create, edit — these are high-value interaction points
20
+ 3. **Map navigation** — how does a user get from page A to page B? What's the happy path?
21
+ 4. **Find auth boundaries** what's public vs protected? Test both sides
22
+ 5. **Check CRUD operations** — can you create, read, update, delete the core entities?
28
23
 
29
- Don't test:
30
- - Framework boilerplate
31
- - Simple getters/setters
32
- - Third-party library behavior
24
+ ## Flow Prioritization
25
+
26
+ | Priority | Description | Examples |
27
+ |----------|-------------|---------|
28
+ | **Critical** | Revenue or access blocking | Login, checkout, payment |
29
+ | **High** | Core product value | Create/edit main entities, dashboard |
30
+ | **Medium** | Secondary features | Settings, profile, search |
31
+ | **Low** | Nice-to-have | Theme toggle, tooltips |
32
+
33
+ Test critical and high flows first. Skip low flows if hitting the token budget.
34
+
35
+ ## Playwright Best Practices
36
+
37
+ ### Locators (prefer resilient selectors)
38
+
39
+ ```typescript
40
+ // GOOD — role-based, resilient to styling changes
41
+ page.getByRole('button', { name: 'Submit' })
42
+ page.getByLabel('Email')
43
+ page.getByText('Welcome back')
44
+ page.getByTestId('user-avatar')
45
+
46
+ // BAD — fragile, breaks on refactoring
47
+ page.locator('.btn-primary')
48
+ page.locator('#submit-btn')
49
+ page.locator('div > form > button:nth-child(2)')
50
+ ```
51
+
52
+ ### Assertions
53
+
54
+ ```typescript
55
+ // Wait for navigation
56
+ await expect(page).toHaveURL('/dashboard');
57
+
58
+ // Wait for element visibility
59
+ await expect(page.getByText('Success')).toBeVisible();
60
+
61
+ // Wait for element to disappear (loading states)
62
+ await expect(page.getByText('Loading...')).not.toBeVisible();
63
+ ```
64
+
65
+ ### Waiting
66
+
67
+ ```typescript
68
+ // GOOD — wait for specific condition
69
+ await page.waitForResponse(resp => resp.url().includes('/api/users'));
70
+ await page.waitForLoadState('networkidle');
71
+
72
+ // BAD — arbitrary delays
73
+ await page.waitForTimeout(3000);
74
+ ```
75
+
76
+ ### Test Structure
77
+
78
+ One flow per file. Each test in the flow tests a step or variant:
79
+
80
+ ```typescript
81
+ test.describe('Checkout flow', () => {
82
+ test('adds item to cart', async ({ page }) => { ... });
83
+ test('fills shipping info', async ({ page }) => { ... });
84
+ test('completes payment', async ({ page }) => { ... });
85
+ test('shows confirmation', async ({ page }) => { ... });
86
+ });
87
+ ```
88
+
89
+ ## What Makes a Good E2E Test
90
+
91
+ | Quality | Good | Bad |
92
+ |---------|------|-----|
93
+ | **User-centric** | Tests what a user would do | Tests implementation details |
94
+ | **Independent** | Each test can run alone | Tests depend on previous test state |
95
+ | **Resilient** | Uses role/label selectors | Uses CSS classes or DOM structure |
96
+ | **Fast-failing** | Fails clearly on the broken step | Fails on a timeout with no context |
97
+ | **Readable** | Test name describes the user action | Test name is a technical description |
98
+
99
+ ## Common Pitfalls
100
+
101
+ 1. **Testing internal state** — don't check Redux store, localStorage, or cookies directly. Test what the user sees.
102
+ 2. **Flaky waits** — use `waitForResponse` or `waitForSelector`, never `waitForTimeout`.
103
+ 3. **Shared state** — each test should set up its own state. Don't rely on test execution order.
104
+ 4. **Over-testing** — one flow per critical path. Don't test every permutation of a form.
105
+ 5. **Ignoring error states** — test what happens when the API returns an error, the network is slow, or the user enters invalid data.
106
+
107
+ ## Regression Analysis
108
+
109
+ When a previously-passing test fails:
110
+
111
+ 1. **Read the error** — what element wasn't found? What URL didn't match?
112
+ 2. **Check if the app changed** — did a route move? Did a button get renamed?
113
+ 3. **Distinguish bug from change** — if the app intentionally changed, the test needs updating. If not, it's a regression.
114
+ 4. **Record the finding** — update the flow matrix with the new status and reasoning.
@@ -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
+ }