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.
- package/package.json +1 -1
- package/skills/fix-pr/SKILL.md +99 -0
- package/skills/qa-strategy/SKILL.md +103 -21
- package/src/commands/fix-pr.ts +324 -0
- package/src/commands/qa.ts +232 -148
- package/src/commands/supi.ts +2 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schema.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/qa/config.ts +43 -0
- package/src/qa/matrix.ts +84 -0
- package/src/qa/prompt-builder.ts +212 -0
- package/src/qa/scripts/detect-app-type.sh +68 -0
- package/src/qa/scripts/discover-routes.sh +143 -0
- package/src/qa/scripts/ensure-playwright.sh +38 -0
- package/src/qa/scripts/run-e2e-tests.sh +99 -0
- package/src/qa/scripts/start-dev-server.sh +46 -0
- package/src/qa/scripts/stop-dev-server.sh +36 -0
- package/src/qa/session.ts +39 -55
- package/src/qa/types.ts +97 -0
- package/src/storage/fix-pr-sessions.ts +59 -0
- package/src/storage/qa-sessions.ts +9 -9
- package/src/types.ts +1 -70
- package/src/qa/detector.ts +0 -61
- package/src/qa/phases/discovery.ts +0 -34
- package/src/qa/phases/execution.ts +0 -65
- package/src/qa/phases/matrix.ts +0 -41
- package/src/qa/phases/reporting.ts +0 -71
- package/src/qa/report.ts +0 -22
- package/src/qa/runner.ts +0 -46
package/src/commands/qa.ts
CHANGED
|
@@ -1,184 +1,268 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../qa/session.js";
|
|
12
|
-
import { buildDiscoveryPrompt } from "../qa/phases/discovery.js";
|
|
13
|
-
import { buildMatrixPrompt } from "../qa/phases/matrix.js";
|
|
14
|
-
import { buildExecutionPrompt } from "../qa/phases/execution.js";
|
|
15
|
-
import { buildReportingPrompt } from "../qa/phases/reporting.js";
|
|
16
|
-
import type { QaPhase, QaSessionLedger } from "../types.js";
|
|
17
|
-
|
|
18
|
-
const PHASE_LABELS: Record<QaPhase, string> = {
|
|
19
|
-
discovery: "Discovery — Scan for test cases",
|
|
20
|
-
matrix: "Matrix — Build traceability matrix",
|
|
21
|
-
execution: "Execution — Run tests",
|
|
22
|
-
reporting: "Reporting — Generate summary",
|
|
23
|
-
};
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { notifyInfo, notifyError, notifyWarning } from "../notifications/renderer.js";
|
|
5
|
+
import { loadE2eQaConfig, saveE2eQaConfig, DEFAULT_E2E_QA_CONFIG } from "../qa/config.js";
|
|
6
|
+
import { loadE2eMatrix } from "../qa/matrix.js";
|
|
7
|
+
import { createNewE2eSession } from "../qa/session.js";
|
|
8
|
+
import { buildE2eOrchestratorPrompt } from "../qa/prompt-builder.js";
|
|
9
|
+
import { findActiveSession, getSessionDir } from "../storage/qa-sessions.js";
|
|
10
|
+
import type { E2eQaConfig, AppType, E2eRegression } from "../qa/types.js";
|
|
24
11
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async handler(args, ctx) {
|
|
29
|
-
const framework = detectAndCache(ctx.cwd);
|
|
30
|
-
|
|
31
|
-
if (!framework) {
|
|
32
|
-
notifyError(
|
|
33
|
-
ctx,
|
|
34
|
-
"No test framework detected",
|
|
35
|
-
"Configure manually via /supi:config"
|
|
36
|
-
);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
12
|
+
function getScriptsDir(): string {
|
|
13
|
+
return path.join(path.dirname(new URL(import.meta.url).pathname), "..", "qa", "scripts");
|
|
14
|
+
}
|
|
39
15
|
|
|
40
|
-
|
|
41
|
-
|
|
16
|
+
function findSkillPath(skillName: string): string | null {
|
|
17
|
+
const candidates = [
|
|
18
|
+
path.join(process.cwd(), "skills", skillName, "SKILL.md"),
|
|
19
|
+
path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "skills", skillName, "SKILL.md"),
|
|
20
|
+
];
|
|
21
|
+
for (const p of candidates) {
|
|
22
|
+
if (fs.existsSync(p)) return p;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
const APP_TYPE_OPTIONS = [
|
|
28
|
+
"nextjs-app — Next.js App Router",
|
|
29
|
+
"nextjs-pages — Next.js Pages Router",
|
|
30
|
+
"react-router — React with React Router",
|
|
31
|
+
"vite — Vite-based app",
|
|
32
|
+
"express — Express.js server",
|
|
33
|
+
"generic — Other web app",
|
|
34
|
+
];
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
const BROWSER_OPTIONS = [
|
|
37
|
+
"chromium (recommended)",
|
|
38
|
+
"firefox",
|
|
39
|
+
"webkit",
|
|
40
|
+
];
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
sessionOptions.push(`Resume ${activeSession.id} (${next ?? "all phases done"} pending)`);
|
|
55
|
-
}
|
|
42
|
+
const RETRY_OPTIONS = [
|
|
43
|
+
"1",
|
|
44
|
+
"2 (recommended)",
|
|
45
|
+
"3",
|
|
46
|
+
];
|
|
56
47
|
|
|
57
|
-
|
|
48
|
+
async function runSetupWizard(
|
|
49
|
+
ctx: any,
|
|
50
|
+
detectedAppType: string | null,
|
|
51
|
+
detectedDevCommand: string | null,
|
|
52
|
+
detectedPort: number | null,
|
|
53
|
+
): Promise<E2eQaConfig | null> {
|
|
54
|
+
// 1. App type
|
|
55
|
+
const appTypeChoice = await ctx.ui.select(
|
|
56
|
+
"App type",
|
|
57
|
+
APP_TYPE_OPTIONS,
|
|
58
|
+
{ helpText: detectedAppType ? `Auto-detected: ${detectedAppType}` : "Select your web app framework" },
|
|
59
|
+
);
|
|
60
|
+
if (!appTypeChoice) return null;
|
|
61
|
+
const appType = appTypeChoice.split(" ")[0] as AppType;
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// 2. Dev command
|
|
64
|
+
const defaultDev = detectedDevCommand || "npm run dev";
|
|
65
|
+
const devCommand = await ctx.ui.input(
|
|
66
|
+
"Dev server command",
|
|
67
|
+
defaultDev,
|
|
68
|
+
{ helpText: "Command to start your development server" },
|
|
69
|
+
);
|
|
70
|
+
if (devCommand === undefined) return null;
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
// 3. Port
|
|
73
|
+
const defaultPort = String(detectedPort || 3000);
|
|
74
|
+
const portStr = await ctx.ui.input(
|
|
75
|
+
"Dev server port",
|
|
76
|
+
defaultPort,
|
|
77
|
+
{ helpText: "Port your dev server runs on" },
|
|
78
|
+
);
|
|
79
|
+
if (portStr === undefined) return null;
|
|
80
|
+
const port = parseInt(portStr, 10) || 3000;
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
// 4. Browser
|
|
83
|
+
const browserChoice = await ctx.ui.select(
|
|
84
|
+
"Browser for E2E tests",
|
|
85
|
+
BROWSER_OPTIONS,
|
|
86
|
+
{ helpText: "Playwright browser to use" },
|
|
87
|
+
);
|
|
88
|
+
if (!browserChoice) return null;
|
|
89
|
+
const browser = browserChoice.split(" ")[0] as "chromium" | "firefox" | "webkit";
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
// 5. Max retries
|
|
92
|
+
const retryChoice = await ctx.ui.select(
|
|
93
|
+
"Max test retries",
|
|
94
|
+
RETRY_OPTIONS,
|
|
95
|
+
{ helpText: "How many times to retry failing tests" },
|
|
96
|
+
);
|
|
97
|
+
if (!retryChoice) return null;
|
|
98
|
+
const maxRetries = parseInt(retryChoice, 10);
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
return {
|
|
101
|
+
app: {
|
|
102
|
+
type: appType,
|
|
103
|
+
devCommand: devCommand || defaultDev,
|
|
104
|
+
port,
|
|
105
|
+
baseUrl: `http://localhost:${port}`,
|
|
106
|
+
},
|
|
107
|
+
playwright: {
|
|
108
|
+
browser,
|
|
109
|
+
headless: true,
|
|
110
|
+
timeout: 30000,
|
|
111
|
+
},
|
|
112
|
+
execution: {
|
|
113
|
+
maxRetries,
|
|
114
|
+
maxFlows: 20,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
87
118
|
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
export function registerQaCommand(pi: ExtensionAPI): void {
|
|
120
|
+
pi.registerCommand("supi:qa", {
|
|
121
|
+
description: "Run autonomous E2E product testing pipeline with playwright",
|
|
122
|
+
async handler(args, ctx) {
|
|
123
|
+
const scriptsDir = getScriptsDir();
|
|
90
124
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
// ── Step 1: Detect app type ─────────────────────────────────────
|
|
126
|
+
let detectedType: string | null = null;
|
|
127
|
+
let detectedDevCommand: string | null = null;
|
|
128
|
+
let detectedPort: number | null = null;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const detectResult = await pi.exec("bash", [
|
|
132
|
+
path.join(scriptsDir, "detect-app-type.sh"),
|
|
133
|
+
ctx.cwd,
|
|
134
|
+
], { cwd: ctx.cwd });
|
|
135
|
+
|
|
136
|
+
if (detectResult.code === 0) {
|
|
137
|
+
const detected = JSON.parse(detectResult.stdout.trim());
|
|
138
|
+
detectedType = detected.type;
|
|
139
|
+
detectedDevCommand = detected.devCommand;
|
|
140
|
+
detectedPort = detected.port;
|
|
94
141
|
}
|
|
142
|
+
} catch { /* proceed without detection */ }
|
|
143
|
+
|
|
144
|
+
// ── Step 2: Ensure playwright ────────────────────────────────────
|
|
145
|
+
try {
|
|
146
|
+
const pwResult = await pi.exec("bash", [
|
|
147
|
+
path.join(scriptsDir, "ensure-playwright.sh"),
|
|
148
|
+
ctx.cwd,
|
|
149
|
+
], { cwd: ctx.cwd });
|
|
95
150
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
phaseOptions.push(PHASE_LABELS[nextPhase]);
|
|
151
|
+
if (pwResult.code !== 0) {
|
|
152
|
+
notifyWarning(ctx, "Playwright setup issue", "Could not verify playwright installation. The agent will handle it.");
|
|
99
153
|
}
|
|
154
|
+
} catch {
|
|
155
|
+
notifyWarning(ctx, "Playwright check skipped", "Will be handled during execution");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Step 3: Load or create config ────────────────────────────────
|
|
159
|
+
let config = loadE2eQaConfig(ctx.cwd);
|
|
160
|
+
|
|
161
|
+
if (!config && ctx.hasUI) {
|
|
162
|
+
config = await runSetupWizard(ctx, detectedType, detectedDevCommand, detectedPort);
|
|
163
|
+
if (!config) return; // user cancelled
|
|
164
|
+
saveE2eQaConfig(ctx.cwd, config);
|
|
165
|
+
ctx.ui.notify("E2E QA config saved to .omp/supipowers/e2e-qa.json", "info");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!config) {
|
|
169
|
+
// Use defaults with detected values
|
|
170
|
+
config = {
|
|
171
|
+
...DEFAULT_E2E_QA_CONFIG,
|
|
172
|
+
app: {
|
|
173
|
+
type: (detectedType as AppType) || "generic",
|
|
174
|
+
devCommand: detectedDevCommand || "npm run dev",
|
|
175
|
+
port: detectedPort || 3000,
|
|
176
|
+
baseUrl: `http://localhost:${detectedPort || 3000}`,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
100
180
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
181
|
+
// ── Step 4: Check for unresolved regressions ─────────────────────
|
|
182
|
+
const activeSession = findActiveSession(ctx.cwd);
|
|
183
|
+
if (activeSession && activeSession.regressions.length > 0 && ctx.hasUI) {
|
|
184
|
+
const unresolvedRegressions = activeSession.regressions.filter((r: E2eRegression) => !r.resolution);
|
|
185
|
+
if (unresolvedRegressions.length > 0) {
|
|
186
|
+
for (const regression of unresolvedRegressions) {
|
|
187
|
+
const choice = await ctx.ui.select(
|
|
188
|
+
`Regression: ${regression.flowName}`,
|
|
189
|
+
[
|
|
190
|
+
"This is a bug — needs fixing",
|
|
191
|
+
"Behavior changed intentionally — update the test",
|
|
192
|
+
"Skip for now",
|
|
193
|
+
],
|
|
194
|
+
{ helpText: `Was passing, now fails: ${regression.error}` },
|
|
195
|
+
);
|
|
196
|
+
if (!choice) continue;
|
|
197
|
+
if (choice.startsWith("This is a bug")) regression.resolution = "bug";
|
|
198
|
+
else if (choice.startsWith("Behavior changed")) regression.resolution = "intentional-change";
|
|
199
|
+
else regression.resolution = "skipped";
|
|
119
200
|
}
|
|
120
|
-
} else if (nextPhase) {
|
|
121
|
-
// Only one option — just run the next phase
|
|
122
|
-
action = { type: "run-phase", phase: nextPhase };
|
|
123
201
|
}
|
|
124
|
-
} else if (nextPhase) {
|
|
125
|
-
action = { type: "run-phase", phase: nextPhase };
|
|
126
202
|
}
|
|
127
203
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
204
|
+
// ── Step 5: Route discovery ──────────────────────────────────────
|
|
205
|
+
let discoveredRoutes = "";
|
|
206
|
+
try {
|
|
207
|
+
const routeResult = await pi.exec("bash", [
|
|
208
|
+
path.join(scriptsDir, "discover-routes.sh"),
|
|
209
|
+
ctx.cwd,
|
|
210
|
+
config.app.type,
|
|
211
|
+
], { cwd: ctx.cwd });
|
|
132
212
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (action.type === "rerun-failed") {
|
|
137
|
-
ledger = advancePhase(ctx.cwd, ledger, "execution", "running");
|
|
138
|
-
prompt = buildExecutionPrompt(ledger, { failedOnly: true, failedTests });
|
|
139
|
-
notifyInfo(ctx, "QA re-running failed tests", `${failedTests.length} test(s)`);
|
|
140
|
-
} else {
|
|
141
|
-
const phase = action.phase;
|
|
142
|
-
ledger = advancePhase(ctx.cwd, ledger, phase, "running");
|
|
143
|
-
|
|
144
|
-
switch (phase) {
|
|
145
|
-
case "discovery":
|
|
146
|
-
prompt = buildDiscoveryPrompt(framework, ctx.cwd);
|
|
147
|
-
break;
|
|
148
|
-
case "matrix":
|
|
149
|
-
prompt = buildMatrixPrompt(ledger);
|
|
150
|
-
break;
|
|
151
|
-
case "execution":
|
|
152
|
-
prompt = buildExecutionPrompt(ledger);
|
|
153
|
-
break;
|
|
154
|
-
case "reporting":
|
|
155
|
-
prompt = buildReportingPrompt(ledger);
|
|
156
|
-
break;
|
|
213
|
+
if (routeResult.code === 0 && routeResult.stdout.trim()) {
|
|
214
|
+
discoveredRoutes = routeResult.stdout.trim();
|
|
157
215
|
}
|
|
216
|
+
} catch { /* agent will discover routes manually */ }
|
|
158
217
|
|
|
159
|
-
|
|
218
|
+
if (!discoveredRoutes) {
|
|
219
|
+
discoveredRoutes = '{"path": "/", "file": "unknown", "type": "page", "hasForm": false}';
|
|
160
220
|
}
|
|
161
221
|
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
222
|
+
// ── Step 6: Load previous matrix ─────────────────────────────────
|
|
223
|
+
const previousMatrix = loadE2eMatrix(ctx.cwd);
|
|
224
|
+
const matrixJson = previousMatrix ? JSON.stringify(previousMatrix, null, 2) : null;
|
|
225
|
+
|
|
226
|
+
// ── Step 7: Create session ───────────────────────────────────────
|
|
227
|
+
const ledger = createNewE2eSession(ctx.cwd, config);
|
|
228
|
+
const sessionDir = getSessionDir(ctx.cwd, ledger.id);
|
|
229
|
+
|
|
230
|
+
// ── Step 8: Load skill ───────────────────────────────────────────
|
|
231
|
+
let skillContent = "";
|
|
232
|
+
const skillPath = findSkillPath("qa-strategy");
|
|
233
|
+
if (skillPath) {
|
|
234
|
+
try {
|
|
235
|
+
skillContent = fs.readFileSync(skillPath, "utf-8");
|
|
236
|
+
} catch { /* proceed without */ }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Step 9: Build and send prompt ────────────────────────────────
|
|
240
|
+
const routeCount = discoveredRoutes.split("\n").filter(Boolean).length;
|
|
241
|
+
|
|
242
|
+
const prompt = buildE2eOrchestratorPrompt({
|
|
243
|
+
cwd: ctx.cwd,
|
|
244
|
+
appType: config.app,
|
|
245
|
+
sessionDir,
|
|
246
|
+
scriptsDir,
|
|
247
|
+
config,
|
|
248
|
+
discoveredRoutes,
|
|
249
|
+
previousMatrix: matrixJson,
|
|
250
|
+
skillContent,
|
|
251
|
+
});
|
|
174
252
|
|
|
175
253
|
pi.sendMessage(
|
|
176
254
|
{
|
|
177
255
|
customType: "supi-qa",
|
|
178
|
-
content: [{ type: "text", text: prompt
|
|
256
|
+
content: [{ type: "text", text: prompt }],
|
|
179
257
|
display: "none",
|
|
180
258
|
},
|
|
181
|
-
{ deliverAs: "steer" }
|
|
259
|
+
{ deliverAs: "steer" },
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
notifyInfo(
|
|
263
|
+
ctx,
|
|
264
|
+
`E2E QA started: ${config.app.type}`,
|
|
265
|
+
`${routeCount} routes discovered | session ${ledger.id}`,
|
|
182
266
|
);
|
|
183
267
|
},
|
|
184
268
|
});
|
package/src/commands/supi.ts
CHANGED
|
@@ -15,7 +15,8 @@ export function handleSupi(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
|
15
15
|
"/supi:plan — Start collaborative planning",
|
|
16
16
|
"/supi:run — Execute a plan with sub-agents",
|
|
17
17
|
"/supi:review — Run quality gates",
|
|
18
|
-
"/supi:qa —
|
|
18
|
+
"/supi:qa — E2E product testing with Playwright",
|
|
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",
|
package/src/config/defaults.ts
CHANGED
package/src/config/schema.ts
CHANGED
|
@@ -25,6 +25,7 @@ const ConfigSchema = Type.Object({
|
|
|
25
25
|
qa: Type.Object({
|
|
26
26
|
framework: Type.Union([Type.String(), Type.Null()]),
|
|
27
27
|
command: Type.Union([Type.String(), Type.Null()]),
|
|
28
|
+
e2e: Type.Boolean(),
|
|
28
29
|
}),
|
|
29
30
|
release: Type.Object({
|
|
30
31
|
pipeline: Type.Union([Type.String(), Type.Null()]),
|
|
@@ -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
|
+
}
|