supipowers 1.2.4 → 1.2.6
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/bin/install.mjs +5 -1
- package/bin/install.ts +110 -14
- package/bin/local-install.sh +17 -13
- package/package.json +1 -1
- package/src/commands/commit.ts +8 -0
- package/src/commands/fix-pr.ts +20 -49
- package/src/commands/mcp.ts +14 -0
- package/src/commands/model.ts +3 -2
- package/src/commands/plan.ts +8 -9
- package/src/commands/qa.ts +15 -0
- package/src/commands/release.ts +16 -1
- package/src/commands/review.ts +7 -8
- package/src/config/model-resolver.ts +87 -0
- package/src/deps/registry.ts +4 -4
- package/src/fix-pr/config.ts +0 -5
- package/src/fix-pr/prompt-builder.ts +7 -6
- package/src/fix-pr/types.ts +0 -11
- package/src/git/commit.ts +74 -26
- package/src/planning/approval-flow.ts +14 -1
- package/src/platform/omp.ts +5 -2
- package/src/platform/types.ts +2 -1
package/bin/install.mjs
CHANGED
|
@@ -5,7 +5,11 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
|
|
6
6
|
const isWindows = process.platform === "win32";
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
8
|
+
// When invoked via bunx/npx, always force a full install to guarantee
|
|
9
|
+
// node_modules/ and a consistent extension directory. The --force flag
|
|
10
|
+
// bypasses the "already up to date" version check.
|
|
11
|
+
const args = [join(__dirname, "install.ts"), "--force", ...process.argv.slice(2)];
|
|
12
|
+
const result = spawnSync("bun", args, {
|
|
9
13
|
stdio: "inherit",
|
|
10
14
|
env: process.env,
|
|
11
15
|
shell: isWindows,
|
package/bin/install.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { spawnSync } from "node:child_process";
|
|
|
14
14
|
import {
|
|
15
15
|
readFileSync,
|
|
16
16
|
writeFileSync,
|
|
17
|
+
appendFileSync,
|
|
17
18
|
existsSync,
|
|
18
19
|
mkdirSync,
|
|
19
20
|
cpSync,
|
|
@@ -123,10 +124,37 @@ async function exec(cmd: string, args: string[]): Promise<ExecResult> {
|
|
|
123
124
|
return { stdout: r.stdout ?? "", stderr: r.stderr ?? "", code: r.status ?? 1 };
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
// ── CLI Flags
|
|
127
|
+
// ── CLI Flags ────────────────────────────────────────────────────
|
|
127
128
|
|
|
128
129
|
const cliArgs = process.argv.slice(2);
|
|
129
130
|
const skipDeps = cliArgs.includes("--skip-deps");
|
|
131
|
+
const FORCE = cliArgs.includes("--force");
|
|
132
|
+
const DEBUG = cliArgs.includes("--debug");
|
|
133
|
+
|
|
134
|
+
// ── Debug logging ────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
const LOG_FILE = resolve(process.cwd(), "supipowers-install.log");
|
|
137
|
+
|
|
138
|
+
function log(msg: string): void {
|
|
139
|
+
if (!DEBUG) return;
|
|
140
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
141
|
+
appendFileSync(LOG_FILE, line);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (DEBUG) {
|
|
145
|
+
// Start fresh log
|
|
146
|
+
writeFileSync(LOG_FILE, `supipowers installer debug log\n`);
|
|
147
|
+
log(`platform: ${process.platform}`);
|
|
148
|
+
log(`arch: ${process.arch}`);
|
|
149
|
+
log(`bun: ${process.versions?.bun ?? "N/A"}`);
|
|
150
|
+
log(`node: ${process.version}`);
|
|
151
|
+
log(`cwd: ${process.cwd()}`);
|
|
152
|
+
log(`homedir: ${homedir()}`);
|
|
153
|
+
log(`argv: ${JSON.stringify(process.argv)}`);
|
|
154
|
+
log(`__dirname: ${__dirname}`);
|
|
155
|
+
log(`packageRoot will be: ${resolve(__dirname, "..")}`);
|
|
156
|
+
log(`isWindows: ${isWindows}`);
|
|
157
|
+
}
|
|
130
158
|
|
|
131
159
|
// ── Install to platform ──────────────────────────────────────
|
|
132
160
|
|
|
@@ -143,18 +171,29 @@ function installToPlatform(platformDir: string, packageRoot: string): string {
|
|
|
143
171
|
const extDir = join(agentDir, "extensions", "supipowers");
|
|
144
172
|
const installedPkgPath = join(extDir, "package.json");
|
|
145
173
|
|
|
174
|
+
log(`installToPlatform(platformDir=${platformDir}, packageRoot=${packageRoot})`);
|
|
175
|
+
log(` agentDir: ${agentDir}`);
|
|
176
|
+
log(` extDir: ${extDir}`);
|
|
177
|
+
|
|
146
178
|
// Check for existing installation
|
|
147
179
|
let installedVersion: string | null = null;
|
|
148
180
|
if (existsSync(installedPkgPath)) {
|
|
149
181
|
try {
|
|
150
182
|
const installed = JSON.parse(readFileSync(installedPkgPath, "utf8"));
|
|
151
183
|
installedVersion = installed.version;
|
|
184
|
+
log(` existing version: ${installedVersion}`);
|
|
152
185
|
} catch {
|
|
153
|
-
|
|
186
|
+
log(` existing package.json corrupted, treating as fresh install`);
|
|
154
187
|
}
|
|
188
|
+
} else {
|
|
189
|
+
log(` no existing installation found`);
|
|
155
190
|
}
|
|
156
191
|
|
|
157
|
-
|
|
192
|
+
const hasNodeModules = existsSync(join(extDir, "node_modules"));
|
|
193
|
+
log(` node_modules present: ${hasNodeModules}`);
|
|
194
|
+
|
|
195
|
+
if (installedVersion === VERSION && hasNodeModules && !FORCE) {
|
|
196
|
+
log(` already up to date with deps, skipping`);
|
|
158
197
|
note(
|
|
159
198
|
`supipowers v${VERSION} is already installed and up to date.`,
|
|
160
199
|
`Up to date (${platformDir})`,
|
|
@@ -162,9 +201,16 @@ function installToPlatform(platformDir: string, packageRoot: string): string {
|
|
|
162
201
|
return extDir;
|
|
163
202
|
}
|
|
164
203
|
|
|
204
|
+
if (installedVersion === VERSION && !hasNodeModules) {
|
|
205
|
+
log(` same version but node_modules missing — reinstalling deps`);
|
|
206
|
+
}
|
|
207
|
+
if (FORCE) {
|
|
208
|
+
log(` --force flag set, reinstalling`);
|
|
209
|
+
}
|
|
210
|
+
|
|
165
211
|
const action = installedVersion ? "Updating" : "Installing";
|
|
166
212
|
if (installedVersion) {
|
|
167
|
-
note(`v${installedVersion}
|
|
213
|
+
note(`v${installedVersion} \u2192 v${VERSION}`, `Updating supipowers (${platformDir})`);
|
|
168
214
|
}
|
|
169
215
|
|
|
170
216
|
const s = spinner();
|
|
@@ -173,14 +219,40 @@ function installToPlatform(platformDir: string, packageRoot: string): string {
|
|
|
173
219
|
try {
|
|
174
220
|
// Clean previous installation to remove stale files
|
|
175
221
|
if (existsSync(extDir)) {
|
|
222
|
+
log(` removing old extDir`);
|
|
176
223
|
rmSync(extDir, { recursive: true });
|
|
177
224
|
}
|
|
178
225
|
|
|
179
|
-
// Copy extension (src/ + bin/ + package.json)
|
|
226
|
+
// Copy extension (src/ + bin/ + package.json) \u2192 ~/<platform>/agent/extensions/supipowers/
|
|
227
|
+
log(` creating extDir and copying files`);
|
|
180
228
|
mkdirSync(extDir, { recursive: true });
|
|
181
229
|
cpSync(join(packageRoot, "src"), join(extDir, "src"), { recursive: true });
|
|
182
230
|
cpSync(join(packageRoot, "bin"), join(extDir, "bin"), { recursive: true });
|
|
183
231
|
cpSync(join(packageRoot, "package.json"), join(extDir, "package.json"));
|
|
232
|
+
log(` files copied to ${extDir}`);
|
|
233
|
+
|
|
234
|
+
// Rewrite package.json for the installed extension.
|
|
235
|
+
// The npm-published package.json has bin, scripts, prepare, devDeps —
|
|
236
|
+
// all of which cause problems during `bun install` in the extension dir.
|
|
237
|
+
// We keep only what OMP needs (omp.extensions) and the runtime dependencies.
|
|
238
|
+
const sourcePkg = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8"));
|
|
239
|
+
const runtimePkg = {
|
|
240
|
+
name: sourcePkg.name,
|
|
241
|
+
version: sourcePkg.version,
|
|
242
|
+
type: sourcePkg.type,
|
|
243
|
+
omp: sourcePkg.omp,
|
|
244
|
+
dependencies: {
|
|
245
|
+
// Only packages imported at runtime by src/ code:
|
|
246
|
+
// - config/schema.ts → @sinclair/typebox
|
|
247
|
+
// - commands/model.ts, model-picker.ts → @oh-my-pi/pi-ai
|
|
248
|
+
// - commands/model-picker.ts → @oh-my-pi/pi-tui
|
|
249
|
+
"@sinclair/typebox": "*",
|
|
250
|
+
"@oh-my-pi/pi-ai": "*",
|
|
251
|
+
"@oh-my-pi/pi-tui": "*",
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
writeFileSync(join(extDir, "package.json"), JSON.stringify(runtimePkg, null, 2));
|
|
255
|
+
log(` rewrote package.json: ${JSON.stringify(runtimePkg, null, 2)}`);
|
|
184
256
|
|
|
185
257
|
// Copy skills → ~/<platform>/agent/skills/<skillname>/SKILL.md
|
|
186
258
|
const skillsSource = join(packageRoot, "skills");
|
|
@@ -196,17 +268,19 @@ function installToPlatform(platformDir: string, packageRoot: string): string {
|
|
|
196
268
|
}
|
|
197
269
|
}
|
|
198
270
|
|
|
199
|
-
// Install dependencies so the extension's
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
// packages aren't in Bun's global install (e.g. OMP installed via npm).
|
|
271
|
+
// Install runtime dependencies so the extension's imports resolve.
|
|
272
|
+
// Without node_modules/, external imports (@sinclair/typebox, @oh-my-pi/*)
|
|
273
|
+
// fail on systems where these packages aren't in Bun's global install.
|
|
274
|
+
log(` running: bun install (cwd=${extDir})`);
|
|
204
275
|
s.message("Installing extension dependencies...");
|
|
205
|
-
const install = run("bun", ["install"
|
|
276
|
+
const install = run("bun", ["install"], { cwd: extDir });
|
|
277
|
+
log(` bun install exit code: ${install.status}`);
|
|
278
|
+
log(` bun install stdout: ${install.stdout ?? "(null)"}`);
|
|
279
|
+
log(` bun install stderr: ${install.stderr ?? "(null)"}`);
|
|
280
|
+
if (install.error) log(` bun install error: ${install.error.message}`);
|
|
206
281
|
if (install.status !== 0) {
|
|
207
|
-
// Non-fatal: the extension may still work if OMP provides the deps
|
|
208
|
-
//
|
|
209
|
-
// registry might be temporarily unreachable.
|
|
282
|
+
// Non-fatal: the extension may still work if OMP provides the deps
|
|
283
|
+
// via its own module resolution (e.g. Bun global install on macOS).
|
|
210
284
|
note(
|
|
211
285
|
"Could not install extension dependencies.\n" +
|
|
212
286
|
"If /supi commands don't appear in OMP, run:\n" +
|
|
@@ -215,12 +289,23 @@ function installToPlatform(platformDir: string, packageRoot: string): string {
|
|
|
215
289
|
);
|
|
216
290
|
}
|
|
217
291
|
|
|
292
|
+
// Verify node_modules was created
|
|
293
|
+
const nmExists = existsSync(join(extDir, "node_modules"));
|
|
294
|
+
log(` node_modules exists after install: ${nmExists}`);
|
|
295
|
+
if (nmExists) {
|
|
296
|
+
try {
|
|
297
|
+
const nmContents = readdirSync(join(extDir, "node_modules"));
|
|
298
|
+
log(` node_modules top-level: ${nmContents.join(", ")}`);
|
|
299
|
+
} catch { /* ignore */ }
|
|
300
|
+
}
|
|
301
|
+
|
|
218
302
|
s.stop(
|
|
219
303
|
installedVersion
|
|
220
304
|
? `supipowers updated to v${VERSION} (${platformDir})`
|
|
221
305
|
: `supipowers v${VERSION} installed (${platformDir})`,
|
|
222
306
|
);
|
|
223
307
|
} catch (err: unknown) {
|
|
308
|
+
log(` installToPlatform FAILED: ${err instanceof Error ? err.stack : String(err)}`);
|
|
224
309
|
s.stop(`${action} failed (${platformDir})`);
|
|
225
310
|
const message = err instanceof Error ? err.message : `Failed to copy files to ~/${platformDir}/agent/`;
|
|
226
311
|
bail(message);
|
|
@@ -377,9 +462,14 @@ async function main(): Promise<void> {
|
|
|
377
462
|
const piBin = findPiBinary();
|
|
378
463
|
const ompBin = findOmpBinary();
|
|
379
464
|
|
|
465
|
+
log(`findPiBinary() => ${piBin ?? "null"}`);
|
|
466
|
+
log(`findOmpBinary() => ${ompBin ?? "null"}`);
|
|
467
|
+
|
|
380
468
|
const piVer = piBin ? run(piBin, ["--version"]).stdout?.trim() || "unknown" : null;
|
|
381
469
|
const ompVer = ompBin ? run(ompBin, ["--version"]).stdout?.trim() || "unknown" : null;
|
|
382
470
|
|
|
471
|
+
log(`piVer: ${piVer ?? "N/A"}, ompVer: ${ompVer ?? "N/A"}`);
|
|
472
|
+
|
|
383
473
|
const detected: string[] = [];
|
|
384
474
|
if (piBin) detected.push(`Pi ${piVer}`);
|
|
385
475
|
if (ompBin) detected.push(`OMP ${ompVer}`);
|
|
@@ -448,6 +538,8 @@ async function main(): Promise<void> {
|
|
|
448
538
|
// ── Step 3: Install supipowers to each chosen target ──────
|
|
449
539
|
|
|
450
540
|
const packageRoot = resolve(__dirname, "..");
|
|
541
|
+
log(`packageRoot: ${packageRoot}`);
|
|
542
|
+
log(`targets: ${JSON.stringify(targets)}`);
|
|
451
543
|
|
|
452
544
|
for (const target of targets) {
|
|
453
545
|
installToPlatform(target.dir, packageRoot);
|
|
@@ -456,6 +548,10 @@ async function main(): Promise<void> {
|
|
|
456
548
|
await installContextMode(target.dir);
|
|
457
549
|
}
|
|
458
550
|
|
|
551
|
+
if (DEBUG) {
|
|
552
|
+
note(`Debug log written to:\n${LOG_FILE}`, "Debug");
|
|
553
|
+
}
|
|
554
|
+
|
|
459
555
|
// ── Step 4: Unified dependency check (--skip-deps to skip) ──
|
|
460
556
|
|
|
461
557
|
if (skipDeps) {
|
package/bin/local-install.sh
CHANGED
|
@@ -2,38 +2,42 @@
|
|
|
2
2
|
# Install supipowers locally from the current working tree.
|
|
3
3
|
# Usage: ./bin/local-install.sh
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Re-run after pulling new changes
|
|
5
|
+
# Creates a global symlink so `supipowers` CLI works, then runs the
|
|
6
|
+
# installer with --debug to deploy the extension and write a log file.
|
|
7
|
+
# Re-run after pulling new changes.
|
|
8
8
|
|
|
9
9
|
set -euo pipefail
|
|
10
10
|
|
|
11
11
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
12
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
13
13
|
|
|
14
|
-
echo "
|
|
14
|
+
echo "-> Installing supipowers locally from $PROJECT_DIR"
|
|
15
15
|
|
|
16
16
|
# 1. Install dependencies (fast no-op when lock is current)
|
|
17
|
-
echo "
|
|
17
|
+
echo "-> Installing dependencies..."
|
|
18
18
|
cd "$PROJECT_DIR"
|
|
19
19
|
bun install --frozen-lockfile 2>/dev/null || bun install
|
|
20
20
|
|
|
21
21
|
# 2. Create a global symlink via bun link
|
|
22
|
-
|
|
23
|
-
# and Pi/OMP can resolve it by name.
|
|
24
|
-
echo "→ Linking supipowers globally…"
|
|
22
|
+
echo "-> Linking supipowers globally..."
|
|
25
23
|
bun link
|
|
26
24
|
|
|
27
25
|
# 3. Verify the link
|
|
28
26
|
if command -v supipowers &>/dev/null; then
|
|
29
|
-
echo "
|
|
27
|
+
echo "[OK] 'supipowers' CLI is available at $(which supipowers)"
|
|
30
28
|
else
|
|
31
|
-
echo "
|
|
29
|
+
echo "[WARN] CLI not on PATH -- you may need to add bun's global bin to \$PATH:"
|
|
32
30
|
echo " export PATH=\"\$HOME/.bun/bin:\$PATH\""
|
|
33
31
|
fi
|
|
34
32
|
|
|
35
|
-
# 4.
|
|
33
|
+
# 4. Run the installer with --debug to deploy extension + write log
|
|
34
|
+
echo "-> Running installer (--debug mode)..."
|
|
35
|
+
bun run "$PROJECT_DIR/bin/install.ts" --debug --force
|
|
36
|
+
|
|
37
|
+
# 5. Show version and log location
|
|
36
38
|
VERSION=$(node -e "console.log(require('$PROJECT_DIR/package.json').version)")
|
|
37
39
|
echo ""
|
|
38
|
-
echo "
|
|
39
|
-
|
|
40
|
+
echo "[OK] supipowers v${VERSION} installed locally"
|
|
41
|
+
if [ -f "$PROJECT_DIR/supipowers-install.log" ]; then
|
|
42
|
+
echo " Debug log: $PROJECT_DIR/supipowers-install.log"
|
|
43
|
+
fi
|
package/package.json
CHANGED
package/src/commands/commit.ts
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Platform } from "../platform/types.js";
|
|
7
7
|
import type { PlatformContext } from "../platform/types.js";
|
|
8
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
8
9
|
import { analyzeAndCommit } from "../git/commit.js";
|
|
9
10
|
|
|
11
|
+
modelRegistry.register({
|
|
12
|
+
id: "commit",
|
|
13
|
+
category: "command",
|
|
14
|
+
label: "Commit",
|
|
15
|
+
harnessRoleHint: "default",
|
|
16
|
+
});
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* Register the command for autocomplete and /help listing.
|
|
12
20
|
* Actual execution goes through handleCommit via the TUI dispatch.
|
package/src/commands/fix-pr.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Platform } from "../platform/types.js";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { loadFixPrConfig, saveFixPrConfig
|
|
4
|
+
import { loadFixPrConfig, saveFixPrConfig } from "../fix-pr/config.js";
|
|
5
5
|
import { buildFixPrOrchestratorPrompt } from "../fix-pr/prompt-builder.js";
|
|
6
6
|
import type { FixPrConfig, CommentReplyPolicy } from "../fix-pr/types.js";
|
|
7
7
|
import {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../storage/fix-pr-sessions.js";
|
|
13
13
|
import { notifyInfo, notifyError, notifyWarning } from "../notifications/renderer.js";
|
|
14
14
|
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
15
|
-
import { resolveModelForAction, createModelBridge } from "../config/model-resolver.js";
|
|
15
|
+
import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
|
|
16
16
|
import { loadModelConfig } from "../config/model-config.js";
|
|
17
17
|
import { detectBotReviewers } from "../fix-pr/bot-detector.js";
|
|
18
18
|
|
|
@@ -23,6 +23,14 @@ modelRegistry.register({
|
|
|
23
23
|
harnessRoleHint: "default",
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
modelRegistry.register({
|
|
27
|
+
id: "task",
|
|
28
|
+
category: "sub-agent",
|
|
29
|
+
parent: "fix-pr",
|
|
30
|
+
label: "Task (sub-agent)",
|
|
31
|
+
harnessRoleHint: "default",
|
|
32
|
+
});
|
|
33
|
+
|
|
26
34
|
function getScriptsDir(): string {
|
|
27
35
|
return path.join(path.dirname(new URL(import.meta.url).pathname), "..", "fix-pr", "scripts");
|
|
28
36
|
}
|
|
@@ -42,6 +50,12 @@ export function registerFixPrCommand(platform: Platform): void {
|
|
|
42
50
|
platform.registerCommand("supi:fix-pr", {
|
|
43
51
|
description: "Fix PR review comments with token-optimized agent orchestration",
|
|
44
52
|
async handler(args: string | undefined, ctx: any): Promise<void> {
|
|
53
|
+
// Resolve and apply model override early — before any logic that might fail
|
|
54
|
+
const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
|
|
55
|
+
const bridge = createModelBridge(platform);
|
|
56
|
+
const resolved = resolveModelForAction("fix-pr", modelRegistry, modelConfig, bridge);
|
|
57
|
+
await applyModelOverride(platform, ctx, "fix-pr", resolved);
|
|
58
|
+
|
|
45
59
|
// ── Step 1: Detect PR ──────────────────────────────────────────
|
|
46
60
|
let prNumber: number | null = null;
|
|
47
61
|
let repo: string | null = null;
|
|
@@ -178,6 +192,9 @@ export function registerFixPrCommand(platform: Platform): void {
|
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
// ── Step 6: Build and send prompt ──────────────────────────────
|
|
195
|
+
// Resolve task model (sub-agents: planner, fixer). Falls back to fix-pr model.
|
|
196
|
+
const taskResolved = resolveModelForAction("task", modelRegistry, modelConfig, bridge);
|
|
197
|
+
const taskModel = taskResolved.model ?? resolved.model ?? "claude-sonnet-4-6";
|
|
181
198
|
const prompt = buildFixPrOrchestratorPrompt({
|
|
182
199
|
prNumber,
|
|
183
200
|
repo,
|
|
@@ -187,15 +204,9 @@ export function registerFixPrCommand(platform: Platform): void {
|
|
|
187
204
|
config,
|
|
188
205
|
iteration: ledger.iteration,
|
|
189
206
|
skillContent,
|
|
207
|
+
taskModel,
|
|
190
208
|
});
|
|
191
209
|
|
|
192
|
-
// Resolve model for this action
|
|
193
|
-
const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
|
|
194
|
-
const bridge = createModelBridge(platform);
|
|
195
|
-
const resolved = resolveModelForAction("fix-pr", modelRegistry, modelConfig, bridge);
|
|
196
|
-
if (resolved.source !== "main" && platform.setModel && resolved.model) {
|
|
197
|
-
platform.setModel(resolved.model);
|
|
198
|
-
}
|
|
199
210
|
|
|
200
211
|
platform.sendMessage(
|
|
201
212
|
{
|
|
@@ -234,10 +245,6 @@ const ITERATION_OPTIONS = [
|
|
|
234
245
|
"5",
|
|
235
246
|
];
|
|
236
247
|
|
|
237
|
-
const MODEL_TIER_OPTIONS = [
|
|
238
|
-
"high — thorough reasoning, more tokens",
|
|
239
|
-
"low — fast execution, fewer tokens",
|
|
240
|
-
];
|
|
241
248
|
|
|
242
249
|
async function runSetupWizard(ctx: any): Promise<FixPrConfig | null> {
|
|
243
250
|
|
|
@@ -270,46 +277,10 @@ async function runSetupWizard(ctx: any): Promise<FixPrConfig | null> {
|
|
|
270
277
|
if (!iterChoice) return null;
|
|
271
278
|
const maxIterations = parseInt(iterChoice, 10);
|
|
272
279
|
|
|
273
|
-
// 4. Model preferences
|
|
274
|
-
const orchestratorTier = await ctx.ui.select(
|
|
275
|
-
"Orchestrator model tier (assessment & grouping)",
|
|
276
|
-
MODEL_TIER_OPTIONS,
|
|
277
|
-
{ helpText: "Higher tier = more thorough analysis" },
|
|
278
|
-
);
|
|
279
|
-
if (!orchestratorTier) return null;
|
|
280
|
-
|
|
281
|
-
const plannerTier = await ctx.ui.select(
|
|
282
|
-
"Planner model tier (fix planning)",
|
|
283
|
-
MODEL_TIER_OPTIONS,
|
|
284
|
-
{ helpText: "Higher tier = more detailed plans" },
|
|
285
|
-
);
|
|
286
|
-
if (!plannerTier) return null;
|
|
287
|
-
|
|
288
|
-
const fixerTier = await ctx.ui.select(
|
|
289
|
-
"Fixer model tier (code changes)",
|
|
290
|
-
MODEL_TIER_OPTIONS,
|
|
291
|
-
{ helpText: "Lower tier usually sufficient for execution" },
|
|
292
|
-
);
|
|
293
|
-
if (!fixerTier) return null;
|
|
294
|
-
|
|
295
280
|
const config: FixPrConfig = {
|
|
296
281
|
reviewer: { type: "none", triggerMethod: null },
|
|
297
282
|
commentPolicy,
|
|
298
283
|
loop: { delaySeconds, maxIterations },
|
|
299
|
-
models: {
|
|
300
|
-
orchestrator: {
|
|
301
|
-
...DEFAULT_FIX_PR_CONFIG.models.orchestrator,
|
|
302
|
-
tier: orchestratorTier.startsWith("high") ? "high" : "low",
|
|
303
|
-
},
|
|
304
|
-
planner: {
|
|
305
|
-
...DEFAULT_FIX_PR_CONFIG.models.planner,
|
|
306
|
-
tier: plannerTier.startsWith("high") ? "high" : "low",
|
|
307
|
-
},
|
|
308
|
-
fixer: {
|
|
309
|
-
...DEFAULT_FIX_PR_CONFIG.models.fixer,
|
|
310
|
-
tier: fixerTier.startsWith("high") ? "high" : "low",
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
284
|
};
|
|
314
285
|
|
|
315
286
|
return config;
|
package/src/commands/mcp.ts
CHANGED
|
@@ -8,6 +8,16 @@ import { generateReadme, writeReadme, writeToolsCache, generateSkill, writeSkill
|
|
|
8
8
|
import { MCPC_EXIT } from "../mcp/types.js";
|
|
9
9
|
import type { McpTool, ServerConfig, HostMcpServer } from "../mcp/types.js";
|
|
10
10
|
import { lookupMcpServer, pickBestMatch } from "../mcp/registry.js";
|
|
11
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
12
|
+
import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
|
|
13
|
+
import { loadModelConfig } from "../config/model-config.js";
|
|
14
|
+
|
|
15
|
+
modelRegistry.register({
|
|
16
|
+
id: "mcp",
|
|
17
|
+
category: "command",
|
|
18
|
+
label: "MCP",
|
|
19
|
+
harnessRoleHint: "slow",
|
|
20
|
+
});
|
|
11
21
|
|
|
12
22
|
export interface ParsedMcpArgs {
|
|
13
23
|
subcommand?: string;
|
|
@@ -789,6 +799,10 @@ export function registerMcpCommand(platform: Platform): void {
|
|
|
789
799
|
platform.registerCommand("supi:mcp", {
|
|
790
800
|
description: "Manage MCP servers — add, remove, enable, disable, refresh",
|
|
791
801
|
async handler(args: string | undefined, ctx: any) {
|
|
802
|
+
const modelCfg = loadModelConfig(platform.paths, ctx.cwd);
|
|
803
|
+
const bridge = createModelBridge(platform);
|
|
804
|
+
const resolved = resolveModelForAction("mcp", modelRegistry, modelCfg, bridge);
|
|
805
|
+
await applyModelOverride(platform, ctx, "mcp", resolved);
|
|
792
806
|
if (args) {
|
|
793
807
|
// CLI mode — parse and dispatch
|
|
794
808
|
await handleMcpCli(platform, ctx, parseCliArgs(args));
|
package/src/commands/model.ts
CHANGED
|
@@ -39,7 +39,7 @@ function buildDashboard(
|
|
|
39
39
|
bridge: ModelPlatformBridge,
|
|
40
40
|
): string {
|
|
41
41
|
const config = loadModelConfig(paths, cwd);
|
|
42
|
-
const lines: string[] = ["
|
|
42
|
+
const lines: string[] = ["\n Model Configuration\n", ` ${"action".padEnd(20)} ${"model".padEnd(24)} ${"thinking".padEnd(10)} source`];
|
|
43
43
|
|
|
44
44
|
let lastCategory: "command" | "sub-agent" | null = null;
|
|
45
45
|
let lastParent: string | undefined = undefined;
|
|
@@ -56,10 +56,11 @@ function buildDashboard(
|
|
|
56
56
|
const modelDisplay = (resolved.source === "main" && source === "main"
|
|
57
57
|
? "—"
|
|
58
58
|
: resolved.model) ?? "—";
|
|
59
|
+
const thinkingDisplay = resolved.thinkingLevel ?? "—";
|
|
59
60
|
const sourceDisplay = formatSource(source);
|
|
60
61
|
|
|
61
62
|
lines.push(
|
|
62
|
-
` ${action.id.padEnd(20)} ${modelDisplay.padEnd(
|
|
63
|
+
` ${action.id.padEnd(20)} ${modelDisplay.padEnd(24)} ${thinkingDisplay.padEnd(10)} ${sourceDisplay}`,
|
|
63
64
|
);
|
|
64
65
|
|
|
65
66
|
lastCategory = action.category;
|
package/src/commands/plan.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { buildPlanningPrompt, buildQuickPlanPrompt } from "../planning/prompt-bu
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
15
|
-
import { resolveModelForAction, createModelBridge } from "../config/model-resolver.js";
|
|
15
|
+
import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
|
|
16
16
|
import { loadModelConfig } from "../config/model-config.js";
|
|
17
17
|
import { startPlanTracking } from "../planning/approval-flow.js";
|
|
18
18
|
|
|
@@ -38,6 +38,12 @@ export function registerPlanCommand(platform: Platform): void {
|
|
|
38
38
|
platform.registerCommand("supi:plan", {
|
|
39
39
|
description: "Start collaborative planning for a feature or task",
|
|
40
40
|
async handler(args: string | undefined, ctx: any) {
|
|
41
|
+
// Resolve and apply model override early — before any logic that might fail
|
|
42
|
+
const modelCfg = loadModelConfig(platform.paths, ctx.cwd);
|
|
43
|
+
const bridge = createModelBridge(platform);
|
|
44
|
+
const resolved = resolveModelForAction("plan", modelRegistry, modelCfg, bridge);
|
|
45
|
+
await applyModelOverride(platform, ctx, "plan", resolved);
|
|
46
|
+
|
|
41
47
|
const skillPath = findSkillPath("planning");
|
|
42
48
|
let skillContent = "";
|
|
43
49
|
if (skillPath) {
|
|
@@ -133,13 +139,6 @@ export function registerPlanCommand(platform: Platform): void {
|
|
|
133
139
|
prompt += "\n\n" + buildVisualInstructions(visualUrl, visualSessionDir);
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
// Resolve model for this action
|
|
137
|
-
const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
|
|
138
|
-
const bridge = createModelBridge(platform);
|
|
139
|
-
const resolved = resolveModelForAction("plan", modelRegistry, modelConfig, bridge);
|
|
140
|
-
if (resolved.source !== "main" && platform.setModel && resolved.model) {
|
|
141
|
-
platform.setModel(resolved.model);
|
|
142
|
-
}
|
|
143
142
|
|
|
144
143
|
platform.sendMessage(
|
|
145
144
|
{
|
|
@@ -151,7 +150,7 @@ export function registerPlanCommand(platform: Platform): void {
|
|
|
151
150
|
);
|
|
152
151
|
|
|
153
152
|
// Track planning state for the approval flow (agent_end hook)
|
|
154
|
-
startPlanTracking(ctx.cwd, platform.paths, ctx.newSession?.bind(ctx));
|
|
153
|
+
startPlanTracking(ctx.cwd, platform.paths, ctx.newSession?.bind(ctx), resolved);
|
|
155
154
|
|
|
156
155
|
notifyInfo(ctx, "Planning started", args ? `Topic: ${args}` : "Describe what you want to build");
|
|
157
156
|
},
|
package/src/commands/qa.ts
CHANGED
|
@@ -8,6 +8,16 @@ import { createNewE2eSession } from "../qa/session.js";
|
|
|
8
8
|
import { buildE2eOrchestratorPrompt } from "../qa/prompt-builder.js";
|
|
9
9
|
import { findActiveSession, getSessionDir } from "../storage/qa-sessions.js";
|
|
10
10
|
import type { E2eQaConfig, AppType, E2eRegression } from "../qa/types.js";
|
|
11
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
12
|
+
import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
|
|
13
|
+
import { loadModelConfig } from "../config/model-config.js";
|
|
14
|
+
|
|
15
|
+
modelRegistry.register({
|
|
16
|
+
id: "qa",
|
|
17
|
+
category: "command",
|
|
18
|
+
label: "QA",
|
|
19
|
+
harnessRoleHint: "slow",
|
|
20
|
+
});
|
|
11
21
|
|
|
12
22
|
function getScriptsDir(): string {
|
|
13
23
|
return path.join(path.dirname(new URL(import.meta.url).pathname), "..", "qa", "scripts");
|
|
@@ -104,6 +114,11 @@ export function registerQaCommand(platform: Platform): void {
|
|
|
104
114
|
platform.registerCommand("supi:qa", {
|
|
105
115
|
description: "Run autonomous E2E product testing pipeline with playwright",
|
|
106
116
|
async handler(args: string | undefined, ctx: any) {
|
|
117
|
+
const modelCfg = loadModelConfig(platform.paths, ctx.cwd);
|
|
118
|
+
const bridge = createModelBridge(platform);
|
|
119
|
+
const resolved = resolveModelForAction("qa", modelRegistry, modelCfg, bridge);
|
|
120
|
+
await applyModelOverride(platform, ctx, "qa", resolved);
|
|
121
|
+
|
|
107
122
|
const scriptsDir = getScriptsDir();
|
|
108
123
|
|
|
109
124
|
// ── Step 1: Detect app type ─────────────────────────────────────
|
package/src/commands/release.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Platform } from "../platform/types.js";
|
|
2
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
3
|
+
import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
|
|
4
|
+
import { loadModelConfig } from "../config/model-config.js";
|
|
2
5
|
import type { ReleaseChannel, BumpType } from "../types.js";
|
|
3
6
|
import { loadConfig, updateConfig } from "../config/loader.js";
|
|
4
7
|
import { detectChannels } from "../release/detector.js";
|
|
@@ -10,6 +13,13 @@ import { notifyInfo, notifySuccess, notifyError } from "../notifications/rendere
|
|
|
10
13
|
import { analyzeAndCommit } from "../git/commit.js";
|
|
11
14
|
import { getWorkingTreeStatus } from "../git/status.js";
|
|
12
15
|
|
|
16
|
+
modelRegistry.register({
|
|
17
|
+
id: "release",
|
|
18
|
+
category: "command",
|
|
19
|
+
label: "Release",
|
|
20
|
+
harnessRoleHint: "slow",
|
|
21
|
+
});
|
|
22
|
+
|
|
13
23
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
14
24
|
const STATUS_KEY = "supi-release";
|
|
15
25
|
const WIDGET_KEY = "supi-release";
|
|
@@ -185,7 +195,12 @@ export function registerReleaseCommand(platform: Platform): void {
|
|
|
185
195
|
* TUI-only handler — called from the input event dispatcher in bootstrap.ts.
|
|
186
196
|
* Runs the full release flow without triggering the outer LLM session.
|
|
187
197
|
*/
|
|
188
|
-
export function handleRelease(platform: Platform, ctx: any, args?: string): void {
|
|
198
|
+
export async function handleRelease(platform: Platform, ctx: any, args?: string): Promise<void> {
|
|
199
|
+
const modelCfg = loadModelConfig(platform.paths, ctx.cwd);
|
|
200
|
+
const bridge = createModelBridge(platform);
|
|
201
|
+
const resolved = resolveModelForAction("release", modelRegistry, modelCfg, bridge);
|
|
202
|
+
await applyModelOverride(platform, ctx, "release", resolved);
|
|
203
|
+
|
|
189
204
|
if (!ctx.hasUI) {
|
|
190
205
|
ctx.ui.notify("Release requires interactive mode", "warning");
|
|
191
206
|
return;
|
package/src/commands/review.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { buildReviewPrompt } from "../quality/gate-runner.js";
|
|
|
5
5
|
import { isLspAvailable } from "../lsp/detector.js";
|
|
6
6
|
import { notifyInfo, notifyWarning } from "../notifications/renderer.js";
|
|
7
7
|
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
8
|
-
import { resolveModelForAction, createModelBridge } from "../config/model-resolver.js";
|
|
8
|
+
import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
|
|
9
9
|
import { loadModelConfig } from "../config/model-config.js";
|
|
10
10
|
|
|
11
11
|
modelRegistry.register({
|
|
@@ -19,6 +19,12 @@ export function registerReviewCommand(platform: Platform): void {
|
|
|
19
19
|
platform.registerCommand("supi:review", {
|
|
20
20
|
description: "Run quality gates at chosen depth (quick/thorough/full-regression)",
|
|
21
21
|
async handler(args: string | undefined, ctx: any) {
|
|
22
|
+
// Resolve and apply model override early — before any logic that might fail
|
|
23
|
+
const modelCfg = loadModelConfig(platform.paths, ctx.cwd);
|
|
24
|
+
const bridge = createModelBridge(platform);
|
|
25
|
+
const resolved = resolveModelForAction("review", modelRegistry, modelCfg, bridge);
|
|
26
|
+
await applyModelOverride(platform, ctx, "review", resolved);
|
|
27
|
+
|
|
22
28
|
const config = loadConfig(platform.paths, ctx.cwd);
|
|
23
29
|
|
|
24
30
|
let profileOverride: string | undefined;
|
|
@@ -96,13 +102,6 @@ export function registerReviewCommand(platform: Platform): void {
|
|
|
96
102
|
|
|
97
103
|
notifyInfo(ctx, `Review started`, `profile: ${profile.name}`);
|
|
98
104
|
|
|
99
|
-
// Resolve model for this action
|
|
100
|
-
const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
|
|
101
|
-
const bridge = createModelBridge(platform);
|
|
102
|
-
const resolved = resolveModelForAction("review", modelRegistry, modelConfig, bridge);
|
|
103
|
-
if (resolved.source !== "main" && platform.setModel && resolved.model) {
|
|
104
|
-
platform.setModel(resolved.model);
|
|
105
|
-
}
|
|
106
105
|
|
|
107
106
|
platform.sendMessage(
|
|
108
107
|
{
|
|
@@ -111,3 +111,90 @@ export function createModelBridge(platform: Platform): ModelPlatformBridge {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Apply a resolved model override to the current session.
|
|
117
|
+
*
|
|
118
|
+
* Resolves the string model ID to an OMP Model object via the context's
|
|
119
|
+
* modelRegistry, then calls platform.setModel() with it. Also applies
|
|
120
|
+
* thinking level if specified.
|
|
121
|
+
*
|
|
122
|
+
* @param platform - The platform adapter
|
|
123
|
+
* @param ctx - Command handler context (must have modelRegistry.getAvailable())
|
|
124
|
+
* @param actionId - The action being configured (e.g. "plan", "review") — used in notification
|
|
125
|
+
* @param resolved - The resolved model from resolveModelForAction()
|
|
126
|
+
* @returns true if model was applied, false if skipped or failed
|
|
127
|
+
*/
|
|
128
|
+
export async function applyModelOverride(
|
|
129
|
+
platform: Platform,
|
|
130
|
+
ctx: any,
|
|
131
|
+
actionId: string,
|
|
132
|
+
resolved: ResolvedModel,
|
|
133
|
+
): Promise<boolean> {
|
|
134
|
+
// Skip if resolution fell through to the main session model (nothing to change)
|
|
135
|
+
if (resolved.source === "main") return false;
|
|
136
|
+
|
|
137
|
+
const modelId = resolved.model;
|
|
138
|
+
if (!modelId) return false;
|
|
139
|
+
|
|
140
|
+
// Apply thinking level (independent of model switch success)
|
|
141
|
+
if (resolved.thinkingLevel && platform.setThinkingLevel) {
|
|
142
|
+
platform.setThinkingLevel(resolved.thinkingLevel);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!platform.setModel) return false;
|
|
146
|
+
|
|
147
|
+
// Resolve string model ID to full OMP Model object via the context's model registry.
|
|
148
|
+
// OMP's setModel expects a Model object (with provider, id, api, etc.), not a string.
|
|
149
|
+
const available = ctx.modelRegistry?.getAvailable?.() as any[] | undefined;
|
|
150
|
+
if (!available) return false;
|
|
151
|
+
|
|
152
|
+
const modelObj = available.find((m: any) => {
|
|
153
|
+
if (!m?.id) return false;
|
|
154
|
+
if (modelId === m.id) return true;
|
|
155
|
+
if (modelId === `${m.provider}/${m.id}`) return true;
|
|
156
|
+
return modelId.includes("/") ? false : m.id === modelId;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!modelObj) return false;
|
|
160
|
+
|
|
161
|
+
// Save current model so we can restore after the agent turn completes.
|
|
162
|
+
// OMP's extension API setModel() persists to settings (calls session.setModel,
|
|
163
|
+
// not session.setModelTemporary). We must restore to avoid permanently
|
|
164
|
+
// overriding the user's default model.
|
|
165
|
+
const originalModel = ctx.model;
|
|
166
|
+
|
|
167
|
+
const applied = await platform.setModel(modelObj);
|
|
168
|
+
if (!applied) return false;
|
|
169
|
+
|
|
170
|
+
// Show persistent model override info in the footer status bar.
|
|
171
|
+
// ctx.ui.notify() is transient and gets immediately replaced by progress widgets;
|
|
172
|
+
// setStatus persists alongside them.
|
|
173
|
+
const STATUS_KEY = "supi-model";
|
|
174
|
+
const displayName = modelObj.name ?? modelObj.id ?? modelId;
|
|
175
|
+
const sourceLabel =
|
|
176
|
+
resolved.source === "action" ? `configured for ${actionId}` :
|
|
177
|
+
resolved.source === "default" ? "supipowers default" :
|
|
178
|
+
"harness role";
|
|
179
|
+
let detail = sourceLabel;
|
|
180
|
+
if (resolved.thinkingLevel) {
|
|
181
|
+
detail += ` \u00b7 ${resolved.thinkingLevel} thinking`;
|
|
182
|
+
}
|
|
183
|
+
ctx.ui?.setStatus?.(STATUS_KEY, `Model: ${displayName} (${detail})`);
|
|
184
|
+
|
|
185
|
+
// Register a one-shot agent_end hook to restore the original model
|
|
186
|
+
// and clear the status bar entry.
|
|
187
|
+
{
|
|
188
|
+
let restored = false;
|
|
189
|
+
platform.on("agent_end", async () => {
|
|
190
|
+
if (restored) return;
|
|
191
|
+
restored = true;
|
|
192
|
+
ctx.ui?.setStatus?.(STATUS_KEY, undefined);
|
|
193
|
+
if (originalModel) {
|
|
194
|
+
await platform.setModel!(originalModel);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return true;
|
|
200
|
+
}
|
package/src/deps/registry.ts
CHANGED
|
@@ -34,10 +34,10 @@ export async function checkBinary(
|
|
|
34
34
|
exec: ExecFn,
|
|
35
35
|
binary: string,
|
|
36
36
|
): Promise<{ installed: boolean; version?: string }> {
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
37
|
+
// Bun.which() is cross-platform (handles .cmd/.exe/.bat on Windows)
|
|
38
|
+
// and doesn't require shelling out to `which` (Unix) or `where` (Windows).
|
|
39
|
+
const found = Bun.which(binary);
|
|
40
|
+
if (!found) return { installed: false };
|
|
41
41
|
|
|
42
42
|
const ver = await exec(binary, ["--version"]);
|
|
43
43
|
const version = ver.code === 0 ? ver.stdout.trim().split("\n")[0] : undefined;
|
package/src/fix-pr/config.ts
CHANGED
|
@@ -13,11 +13,6 @@ export const DEFAULT_FIX_PR_CONFIG: FixPrConfig = {
|
|
|
13
13
|
reviewer: { type: "none", triggerMethod: null },
|
|
14
14
|
commentPolicy: "answer-selective",
|
|
15
15
|
loop: { delaySeconds: 180, maxIterations: 3 },
|
|
16
|
-
models: {
|
|
17
|
-
orchestrator: { provider: "anthropic", model: "claude-opus-4-6", tier: "high" },
|
|
18
|
-
planner: { provider: "anthropic", model: "claude-opus-4-6", tier: "high" },
|
|
19
|
-
fixer: { provider: "anthropic", model: "claude-sonnet-4-6", tier: "low" },
|
|
20
|
-
},
|
|
21
16
|
};
|
|
22
17
|
|
|
23
18
|
export function loadFixPrConfig(paths: PlatformPaths, cwd: string): FixPrConfig | null {
|
|
@@ -10,6 +10,8 @@ export interface FixPrPromptOptions {
|
|
|
10
10
|
config: FixPrConfig;
|
|
11
11
|
iteration: number;
|
|
12
12
|
skillContent: string;
|
|
13
|
+
/** Resolved model ID for sub-agent tasks (planner, fixer roles). */
|
|
14
|
+
taskModel: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function buildReplyInstructions(config: FixPrConfig): string {
|
|
@@ -47,8 +49,8 @@ function buildReplyInstructions(config: FixPrConfig): string {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): string {
|
|
50
|
-
const { prNumber, repo, comments, sessionDir, scriptsDir, config, iteration, skillContent } = options;
|
|
51
|
-
const { loop,
|
|
52
|
+
const { prNumber, repo, comments, sessionDir, scriptsDir, config, iteration, skillContent, taskModel } = options;
|
|
53
|
+
const { loop, reviewer } = config;
|
|
52
54
|
const maxIter = loop.maxIterations;
|
|
53
55
|
const delay = loop.delaySeconds;
|
|
54
56
|
|
|
@@ -190,11 +192,10 @@ export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): strin
|
|
|
190
192
|
sections.push(
|
|
191
193
|
"## Model Guidance",
|
|
192
194
|
"",
|
|
193
|
-
`- **Orchestrator** (
|
|
194
|
-
`- **Planner** (
|
|
195
|
-
`- **Fixer** (code changes): ${models.fixer.model} (${models.fixer.tier} tier) — focused execution`,
|
|
195
|
+
`- **Orchestrator** (this session): handles assessment & grouping`,
|
|
196
|
+
`- **Planner & Fixer** (sub-agents): use model \`${taskModel}\``,
|
|
196
197
|
"",
|
|
197
|
-
"
|
|
198
|
+
"Sub-agents inherit the task model for planning and code changes.",
|
|
198
199
|
);
|
|
199
200
|
|
|
200
201
|
return sections.join("\n");
|
package/src/fix-pr/types.ts
CHANGED
|
@@ -4,12 +4,6 @@ export type ReviewerType = "coderabbit" | "copilot" | "gemini" | "none";
|
|
|
4
4
|
/** How to handle comment replies */
|
|
5
5
|
export type CommentReplyPolicy = "answer-all" | "answer-selective" | "no-answer";
|
|
6
6
|
|
|
7
|
-
/** Model preference for a specific role */
|
|
8
|
-
export interface ModelPref {
|
|
9
|
-
provider: string;
|
|
10
|
-
model: string;
|
|
11
|
-
tier: "low" | "high";
|
|
12
|
-
}
|
|
13
7
|
|
|
14
8
|
/** Per-repo fix-pr configuration */
|
|
15
9
|
export interface FixPrConfig {
|
|
@@ -22,11 +16,6 @@ export interface FixPrConfig {
|
|
|
22
16
|
delaySeconds: number;
|
|
23
17
|
maxIterations: number;
|
|
24
18
|
};
|
|
25
|
-
models: {
|
|
26
|
-
orchestrator: ModelPref;
|
|
27
|
-
planner: ModelPref;
|
|
28
|
-
fixer: ModelPref;
|
|
29
|
-
};
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
/** A PR review comment from GitHub API */
|
package/src/git/commit.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { validateCommitMessage } from "./commit-msg.js";
|
|
|
10
10
|
import { getWorkingTreeStatus } from "./status.js";
|
|
11
11
|
import { discoverCommitConventions } from "./conventions.js";
|
|
12
12
|
import { notifyInfo, notifyError, notifySuccess } from "../notifications/renderer.js";
|
|
13
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
14
|
+
import { resolveModelForAction, createModelBridge } from "../config/model-resolver.js";
|
|
15
|
+
import { loadModelConfig } from "../config/model-config.js";
|
|
13
16
|
|
|
14
17
|
// ── Public types ───────────────────────────────────────────
|
|
15
18
|
|
|
@@ -213,6 +216,7 @@ function createProgress(ctx: any) {
|
|
|
213
216
|
dispose() {
|
|
214
217
|
stopTimer();
|
|
215
218
|
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
219
|
+
ctx.ui.setStatus?.("supi-model", undefined);
|
|
216
220
|
ctx.ui.setWidget?.(WIDGET_KEY, undefined);
|
|
217
221
|
},
|
|
218
222
|
};
|
|
@@ -292,8 +296,26 @@ export async function analyzeAndCommit(
|
|
|
292
296
|
|
|
293
297
|
if (platform.capabilities.agentSessions) {
|
|
294
298
|
progress.activate(4, `${fileList.length} file(s)`);
|
|
295
|
-
|
|
299
|
+
// Resolve the commit sub-agent model from config (falls back to session default)
|
|
300
|
+
const modelCfg = loadModelConfig(platform.paths, cwd);
|
|
301
|
+
const bridge = createModelBridge(platform);
|
|
302
|
+
const commitModel = resolveModelForAction("commit", modelRegistry, modelCfg, bridge);
|
|
303
|
+
|
|
304
|
+
// Show model override in status bar if not using the main session model
|
|
305
|
+
if (commitModel.source !== "main" && commitModel.model) {
|
|
306
|
+
const sourceLabel =
|
|
307
|
+
commitModel.source === "action" ? "configured for commit" :
|
|
308
|
+
commitModel.source === "default" ? "supipowers default" :
|
|
309
|
+
"harness role";
|
|
310
|
+
let detail = sourceLabel;
|
|
311
|
+
if (commitModel.thinkingLevel) {
|
|
312
|
+
detail += ` \u00b7 ${commitModel.thinkingLevel} thinking`;
|
|
313
|
+
}
|
|
314
|
+
ctx.ui?.setStatus?.("supi-model", `Model: ${commitModel.model} (${detail})`);
|
|
315
|
+
}
|
|
316
|
+
plan = await tryAgentPlan(platform, cwd, prompt, commitModel.model);
|
|
296
317
|
if (plan) {
|
|
318
|
+
plan = validatePlanFiles(plan, fileList);
|
|
297
319
|
progress.complete(4, `${plan.commits.length} commit(s)`);
|
|
298
320
|
} else {
|
|
299
321
|
progress.skip(4, "unavailable");
|
|
@@ -332,7 +354,7 @@ export async function analyzeAndCommit(
|
|
|
332
354
|
|
|
333
355
|
// 7. Execute commits
|
|
334
356
|
progress.activate(6, `0/${plan.commits.length}`);
|
|
335
|
-
return executeCommitPlan(platform, ctx, cwd, plan, progress);
|
|
357
|
+
return executeCommitPlan(platform, ctx, cwd, plan, fileList, progress);
|
|
336
358
|
} finally {
|
|
337
359
|
// Always clean up, even on unexpected errors
|
|
338
360
|
progress.dispose();
|
|
@@ -345,10 +367,11 @@ async function tryAgentPlan(
|
|
|
345
367
|
platform: Platform,
|
|
346
368
|
cwd: string,
|
|
347
369
|
prompt: string,
|
|
370
|
+
model?: string,
|
|
348
371
|
): Promise<CommitPlan | null> {
|
|
349
372
|
let session: Awaited<ReturnType<Platform["createAgentSession"]>> | null = null;
|
|
350
373
|
try {
|
|
351
|
-
session = await platform.createAgentSession({ cwd, hasUI: false });
|
|
374
|
+
session = await platform.createAgentSession({ cwd, hasUI: false, ...(model ? { model } : {}) });
|
|
352
375
|
|
|
353
376
|
const agentDone = new Promise<void>((resolve) => {
|
|
354
377
|
session!.subscribe((event: any) => {
|
|
@@ -426,6 +449,26 @@ async function manualFallback(
|
|
|
426
449
|
return { committed: 1, messages: [message] };
|
|
427
450
|
}
|
|
428
451
|
|
|
452
|
+
// ── Plan validation ────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Filter an AI-generated commit plan against the actual staged file list.
|
|
456
|
+
* Removes hallucinated paths that aren't staged, and drops empty groups.
|
|
457
|
+
* Falls back to the original plan if filtering would leave nothing.
|
|
458
|
+
*/
|
|
459
|
+
export function validatePlanFiles(plan: CommitPlan, stagedFiles: string[]): CommitPlan {
|
|
460
|
+
const stagedSet = new Set(stagedFiles);
|
|
461
|
+
const validCommits = plan.commits
|
|
462
|
+
.map((group) => ({
|
|
463
|
+
...group,
|
|
464
|
+
files: group.files.filter((f) => stagedSet.has(f)),
|
|
465
|
+
}))
|
|
466
|
+
.filter((group) => group.files.length > 0);
|
|
467
|
+
|
|
468
|
+
return validCommits.length > 0 ? { commits: validCommits } : plan;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
429
472
|
// ── Commit execution ───────────────────────────────────────
|
|
430
473
|
|
|
431
474
|
async function executeCommitPlan(
|
|
@@ -433,38 +476,45 @@ async function executeCommitPlan(
|
|
|
433
476
|
ctx: any,
|
|
434
477
|
cwd: string,
|
|
435
478
|
plan: CommitPlan,
|
|
479
|
+
stagedFiles: string[],
|
|
436
480
|
progress: ReturnType<typeof createProgress>,
|
|
437
481
|
): Promise<CommitResult | null> {
|
|
438
482
|
const exec = platform.exec.bind(platform);
|
|
439
483
|
const committedMessages: string[] = [];
|
|
440
484
|
|
|
485
|
+
// Snapshot the full index as a tree object. This lets us restore the
|
|
486
|
+
// staging area for each commit group via `git read-tree` — which reads
|
|
487
|
+
// from git's object store and never consults .gitignore.
|
|
488
|
+
const writeTreeResult = await exec("git", ["write-tree"], { cwd });
|
|
489
|
+
if (writeTreeResult.code !== 0) {
|
|
490
|
+
progress.dispose();
|
|
491
|
+
notifyError(ctx, "Commit failed", "Could not snapshot index (git write-tree)");
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const savedTree = writeTreeResult.stdout.trim();
|
|
495
|
+
|
|
441
496
|
for (let i = 0; i < plan.commits.length; i++) {
|
|
442
497
|
const group = plan.commits[i];
|
|
443
498
|
const header = formatCommitMessage(group).split("\n")[0];
|
|
444
499
|
progress.detail(`${i + 1}/${plan.commits.length}: ${header}`);
|
|
445
500
|
|
|
446
|
-
//
|
|
447
|
-
await exec("git", ["
|
|
501
|
+
// Restore the full saved index (no gitignore involvement)
|
|
502
|
+
await exec("git", ["read-tree", savedTree], { cwd });
|
|
448
503
|
|
|
449
|
-
//
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const reason = addResult.stderr?.trim() || "git add returned non-zero";
|
|
455
|
-
return reportPartialFailure(ctx, exec, cwd, committedMessages, {
|
|
456
|
-
step: `Commit ${i + 1}/${plan.commits.length}`,
|
|
457
|
-
error: `Could not stage files (${failedFiles}): ${reason}`,
|
|
458
|
-
});
|
|
504
|
+
// Unstage everything NOT in this group
|
|
505
|
+
const groupSet = new Set(group.files);
|
|
506
|
+
const filesToUnstage = stagedFiles.filter((f) => !groupSet.has(f));
|
|
507
|
+
if (filesToUnstage.length > 0) {
|
|
508
|
+
await exec("git", ["reset", "HEAD", "--", ...filesToUnstage], { cwd });
|
|
459
509
|
}
|
|
460
510
|
|
|
461
|
-
// Build commit message
|
|
462
511
|
const message = formatCommitMessage(group);
|
|
463
|
-
|
|
464
512
|
const commitResult = await commitStaged(exec, cwd, message);
|
|
465
513
|
if (!commitResult.success) {
|
|
466
514
|
progress.dispose();
|
|
467
|
-
|
|
515
|
+
// Restore full staging area so the user isn't left with a partial index
|
|
516
|
+
await exec("git", ["read-tree", savedTree], { cwd });
|
|
517
|
+
return reportPartialFailure(ctx, committedMessages, {
|
|
468
518
|
step: `Commit ${i + 1}/${plan.commits.length}`,
|
|
469
519
|
error: commitResult.error!,
|
|
470
520
|
});
|
|
@@ -473,6 +523,10 @@ async function executeCommitPlan(
|
|
|
473
523
|
committedMessages.push(message);
|
|
474
524
|
}
|
|
475
525
|
|
|
526
|
+
// Restore the saved index so any staged files NOT in the plan remain staged.
|
|
527
|
+
// Files already committed now match HEAD, so they appear as not-staged.
|
|
528
|
+
await exec("git", ["read-tree", savedTree], { cwd });
|
|
529
|
+
|
|
476
530
|
progress.complete(6, `${committedMessages.length} done`);
|
|
477
531
|
progress.dispose();
|
|
478
532
|
notifySuccess(
|
|
@@ -486,18 +540,12 @@ async function executeCommitPlan(
|
|
|
486
540
|
|
|
487
541
|
/**
|
|
488
542
|
* Report a mid-plan failure with context on what succeeded and what failed.
|
|
489
|
-
* Re-stages remaining files so the user isn't left with a half-reset index.
|
|
490
543
|
*/
|
|
491
|
-
|
|
544
|
+
function reportPartialFailure(
|
|
492
545
|
ctx: any,
|
|
493
|
-
exec: Platform["exec"],
|
|
494
|
-
cwd: string,
|
|
495
546
|
committedMessages: string[],
|
|
496
547
|
failure: { step: string; error: string },
|
|
497
|
-
):
|
|
498
|
-
// Re-stage everything so the user isn't stuck with a partial index
|
|
499
|
-
await exec("git", ["add", "-A"], { cwd });
|
|
500
|
-
|
|
548
|
+
): CommitResult | null {
|
|
501
549
|
const lines: string[] = [];
|
|
502
550
|
lines.push(`Failed at ${failure.step}: ${failure.error}`);
|
|
503
551
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Platform } from "../platform/types.js";
|
|
2
|
+
import type { ResolvedModel } from "../types.js";
|
|
3
|
+
import { applyModelOverride } from "../config/model-resolver.js";
|
|
2
4
|
import { listPlans, readPlanFile } from "../storage/plans.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -14,17 +16,21 @@ let plansBefore: string[] = [];
|
|
|
14
16
|
let planCwd: string = "";
|
|
15
17
|
/** newSession function captured from the command context at plan start. */
|
|
16
18
|
let capturedNewSession: ((options?: any) => Promise<{ cancelled: boolean }>) | null = null;
|
|
19
|
+
/** Resolved model for plan action — re-applied on execution handoff. */
|
|
20
|
+
let capturedResolvedModel: ResolvedModel | null = null;
|
|
17
21
|
|
|
18
22
|
/** Mark planning as started (called by plan command after sending steer). */
|
|
19
23
|
export function startPlanTracking(
|
|
20
24
|
cwd: string,
|
|
21
25
|
paths: any,
|
|
22
26
|
newSession?: (options?: any) => Promise<{ cancelled: boolean }>,
|
|
27
|
+
resolvedModel?: ResolvedModel,
|
|
23
28
|
): void {
|
|
24
29
|
planningActive = true;
|
|
25
30
|
planCwd = cwd;
|
|
26
31
|
plansBefore = listPlans(paths, cwd);
|
|
27
32
|
capturedNewSession = newSession ?? null;
|
|
33
|
+
capturedResolvedModel = resolvedModel ?? null;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/** Cancel plan tracking (e.g., session change). */
|
|
@@ -33,6 +39,7 @@ export function cancelPlanTracking(): void {
|
|
|
33
39
|
plansBefore = [];
|
|
34
40
|
planCwd = "";
|
|
35
41
|
capturedNewSession = null;
|
|
42
|
+
capturedResolvedModel = null;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/** Whether a planning session is currently active. */
|
|
@@ -89,10 +96,16 @@ async function executeApproveFlow(
|
|
|
89
96
|
): Promise<void> {
|
|
90
97
|
const prompt = buildExecutionPrompt(planContent, planPath);
|
|
91
98
|
|
|
99
|
+
// Re-apply the plan model override for the execution turn.
|
|
100
|
+
// The planning turn's restore hook already fired (model reverted to default).
|
|
101
|
+
// We must switch again so the execution LLM turn uses the configured model.
|
|
102
|
+
if (capturedResolvedModel) {
|
|
103
|
+
await applyModelOverride(platform, ctx, "plan", capturedResolvedModel);
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
if (capturedNewSession) {
|
|
93
107
|
const result = await capturedNewSession();
|
|
94
108
|
if (result?.cancelled) {
|
|
95
|
-
// User dismissed the new-session prompt — keep plan state intact.
|
|
96
109
|
ctx.ui.notify("Session start cancelled. Plan saved; run /supi:plan again to execute.");
|
|
97
110
|
return;
|
|
98
111
|
}
|
package/src/platform/omp.ts
CHANGED
|
@@ -17,8 +17,11 @@ export function createOmpAdapter(api: any): Platform {
|
|
|
17
17
|
sendUserMessage: (text: string) => api.sendUserMessage(text),
|
|
18
18
|
registerMessageRenderer: (type, fn) => api.registerMessageRenderer(type, fn),
|
|
19
19
|
|
|
20
|
-
setModel(model:
|
|
21
|
-
api.setModel(model);
|
|
20
|
+
async setModel(model: any): Promise<boolean> {
|
|
21
|
+
return api.setModel(model);
|
|
22
|
+
},
|
|
23
|
+
setThinkingLevel(level: string, persist?: boolean): void {
|
|
24
|
+
api.setThinkingLevel?.(level, persist);
|
|
22
25
|
},
|
|
23
26
|
getCurrentModel(): string {
|
|
24
27
|
return api.getCurrentModel?.() ?? "unknown";
|
package/src/platform/types.ts
CHANGED
|
@@ -138,7 +138,8 @@ export interface Platform {
|
|
|
138
138
|
registerMessageRenderer<T>(type: string, renderer: any): void;
|
|
139
139
|
|
|
140
140
|
// Model access
|
|
141
|
-
setModel?(model:
|
|
141
|
+
setModel?(model: any): Promise<boolean>;
|
|
142
|
+
setThinkingLevel?(level: string, persist?: boolean): void;
|
|
142
143
|
getCurrentModel?(): string;
|
|
143
144
|
getModelForRole?(role: string): string | null;
|
|
144
145
|
|