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 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
- const result = spawnSync("bun", [join(__dirname, "install.ts"), ...process.argv.slice(2)], {
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
- // corrupted package.json treat as not installed
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
- if (installedVersion === VERSION) {
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} v${VERSION}`, `Updating supipowers (${platformDir})`);
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) ~/<platform>/agent/extensions/supipowers/
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 runtime imports resolve.
200
- // Bun installs peer deps by default, which provides @sinclair/typebox
201
- // and @oh-my-pi/* packages that the extension needs at import time.
202
- // Without this, the extension fails to load on systems where these
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", "--frozen-lockfile=false"], { cwd: extDir });
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
- // Log a warning but don't bail the user might be offline or the
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) {
@@ -2,38 +2,42 @@
2
2
  # Install supipowers locally from the current working tree.
3
3
  # Usage: ./bin/local-install.sh
4
4
  #
5
- # This creates a global symlink so both the `supipowers` CLI and
6
- # the Pi/OMP extension resolve to your local source no publish needed.
7
- # Re-run after pulling new changes; the symlink stays valid.
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 " Installing supipowers locally from $PROJECT_DIR"
14
+ echo "-> Installing supipowers locally from $PROJECT_DIR"
15
15
 
16
16
  # 1. Install dependencies (fast no-op when lock is current)
17
- echo " Installing dependencies"
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
- # This registers the package globally so `supipowers` CLI works
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 " 'supipowers' CLI is available at $(which supipowers)"
27
+ echo "[OK] 'supipowers' CLI is available at $(which supipowers)"
30
28
  else
31
- echo " CLI not on PATH you may need to add bun's global bin to \$PATH:"
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. Show version
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 " supipowers v${VERSION} installed locally (linked to $PROJECT_DIR)"
39
- echo " Any edits to src/ or skills/ take effect immediately — no rebuild needed."
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "Workflow extension for OMP coding agents.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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.
@@ -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, DEFAULT_FIX_PR_CONFIG } from "../fix-pr/config.js";
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;
@@ -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));
@@ -39,7 +39,7 @@ function buildDashboard(
39
39
  bridge: ModelPlatformBridge,
40
40
  ): string {
41
41
  const config = loadModelConfig(paths, cwd);
42
- const lines: string[] = ["", " Model Configuration", ""];
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(28)} ${sourceDisplay}`,
63
+ ` ${action.id.padEnd(20)} ${modelDisplay.padEnd(24)} ${thinkingDisplay.padEnd(10)} ${sourceDisplay}`,
63
64
  );
64
65
 
65
66
  lastCategory = action.category;
@@ -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
  },
@@ -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 ─────────────────────────────────────
@@ -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;
@@ -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
+ }
@@ -34,10 +34,10 @@ export async function checkBinary(
34
34
  exec: ExecFn,
35
35
  binary: string,
36
36
  ): Promise<{ installed: boolean; version?: string }> {
37
- // `which` is Unix-only; Windows uses `where` to locate executables
38
- const whichCmd = process.platform === "win32" ? "where" : "which";
39
- const which = await exec(whichCmd, [binary]);
40
- if (which.code !== 0) return { installed: false };
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;
@@ -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, models, reviewer } = config;
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** (assessment, grouping): ${models.orchestrator.model} (${models.orchestrator.tier} tier) — thorough analysis`,
194
- `- **Planner** (fix planning): ${models.planner.model} (${models.planner.tier} tier) — detailed planning`,
195
- `- **Fixer** (code changes): ${models.fixer.model} (${models.fixer.tier} tier) — focused execution`,
195
+ `- **Orchestrator** (this session): handles assessment & grouping`,
196
+ `- **Planner & Fixer** (sub-agents): use model \`${taskModel}\``,
196
197
  "",
197
- "These indicate the expected reasoning depth for each phase of work.",
198
+ "Sub-agents inherit the task model for planning and code changes.",
198
199
  );
199
200
 
200
201
  return sections.join("\n");
@@ -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
- plan = await tryAgentPlan(platform, cwd, prompt);
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
- // Reset staging area
447
- await exec("git", ["reset", "HEAD"], { cwd });
501
+ // Restore the full saved index (no gitignore involvement)
502
+ await exec("git", ["read-tree", savedTree], { cwd });
448
503
 
449
- // Stage only this group's files
450
- const addResult = await exec("git", ["add", ...group.files], { cwd });
451
- if (addResult.code !== 0) {
452
- progress.dispose();
453
- const failedFiles = group.files.join(", ");
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
- return reportPartialFailure(ctx, exec, cwd, committedMessages, {
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
- async function reportPartialFailure(
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
- ): Promise<CommitResult | null> {
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
  }
@@ -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: string): void {
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";
@@ -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: string): void;
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