supipowers 1.2.5 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "1.2.5",
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
+ }
@@ -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