supipowers 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "Workflow extension for OMP coding agents.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -33,10 +33,6 @@ Instead use:
33
33
  - `ctx_execute(language: "shell", code: "find ...")` to run in sandbox
34
34
  - `ctx_batch_execute(commands, queries)` for multiple searches
35
35
 
36
- ### Read (full-file, no limit) — BLOCKED
37
- Reading an entire file without a `limit` parameter is blocked.
38
- - If you need to **Edit** the file → re-call Read with `limit` parameter (e.g., `limit: 200`)
39
- - If you need to **analyze or explore** → use `ctx_execute_file(path, language, code)` instead. Only your printed summary enters context.
40
36
 
41
37
  ## REDIRECTED tools — use sandbox equivalents
42
38
 
@@ -46,9 +42,9 @@ For everything else, use:
46
42
  - `ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
47
43
  - `ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context
48
44
 
49
- ### Read (for analysis)
50
- If you are reading a file to **Edit** it Read with `limit` is correct (Edit needs content in context).
51
- If you are reading to **analyze, explore, or summarize** → use `ctx_execute_file(path, language, code)` instead. Only your printed summary enters context. The raw file content stays in the sandbox.
45
+ ### Read (large files)
46
+ Reads are never blocked — they always go through OMP's native read tool so hashline anchors (`N#XX`) are preserved for the edit contract. Large file reads (>110 lines) are automatically compressed to head (80 lines) + tail (30 lines) with a `sel` hint for the omitted section.
47
+ For analysis-only reads where hashlines aren't needed, `ctx_execute_file(path, language, code)` remains more efficient — only your printed summary enters context.
52
48
 
53
49
  ## Tool selection hierarchy
54
50
 
package/src/bootstrap.ts CHANGED
@@ -17,6 +17,7 @@ import { registerModelCommand, handleModel } from "./commands/model.js";
17
17
  import { executeManagerAction } from "./mcp/manager-tool.js";
18
18
  import { registerFixPrCommand } from "./commands/fix-pr.js";
19
19
  import { registerContextCommand, handleContext } from "./commands/context.js";
20
+ import { registerOptimizeContextCommand, handleOptimizeContext } from "./commands/optimize-context.js";
20
21
  import { registerCommitCommand, handleCommit } from "./commands/commit.js";
21
22
  import { loadConfig } from "./config/loader.js";
22
23
  import { registerContextModeHooks } from "./context-mode/hooks.js";
@@ -37,6 +38,7 @@ const TUI_COMMANDS: Record<string, (platform: Platform, ctx: any, args?: string)
37
38
  "supi:mcp": (platform, ctx) => handleMcp(platform, ctx),
38
39
  "supi:model": (platform, ctx) => handleModel(platform, ctx),
39
40
  "supi:context": (platform, ctx) => handleContext(platform, ctx),
41
+ "supi:optimize-context": (platform, ctx) => handleOptimizeContext(platform, ctx),
40
42
  "supi:commit": (platform, ctx, args) => handleCommit(platform, ctx, args),
41
43
  "supi:release": (platform, ctx, args) => handleRelease(platform, ctx, args),
42
44
  };
@@ -68,6 +70,7 @@ export function bootstrap(platform: Platform): void {
68
70
  registerMcpCommand(platform);
69
71
  registerModelCommand(platform);
70
72
  registerContextCommand(platform);
73
+ registerOptimizeContextCommand(platform);
71
74
  registerCommitCommand(platform);
72
75
 
73
76
 
@@ -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;
@@ -0,0 +1,202 @@
1
+ import type { Platform, PlatformContext } from "../platform/types.js";
2
+ import {
3
+ parseSystemPrompt,
4
+ parseIndividualSkills,
5
+ } from "../context/analyzer.js";
6
+ import {
7
+ detectTechStack,
8
+ buildContextReport,
9
+ } from "../context/optimizer.js";
10
+ import type { ContextReport } from "../context/optimizer.js";
11
+
12
+ function formatTokens(n: number): string {
13
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
14
+ }
15
+
16
+ export function handleOptimizeContext(platform: Platform, ctx: PlatformContext): void {
17
+ void (async () => {
18
+ if (!ctx.hasUI) return;
19
+
20
+ // 1. Detect tech stack
21
+ const techStack = await detectTechStack(platform, ctx.cwd);
22
+
23
+ // 2. Get system prompt
24
+ let systemPrompt = "";
25
+ try {
26
+ systemPrompt = (ctx as any).getSystemPrompt?.() ?? "";
27
+ } catch {
28
+ // getSystemPrompt not available
29
+ }
30
+
31
+ if (!systemPrompt) {
32
+ ctx.ui.notify("System prompt unavailable", "warning");
33
+ return;
34
+ }
35
+
36
+ // 3. Parse
37
+ const sections = parseSystemPrompt(systemPrompt);
38
+ const skills = parseIndividualSkills(systemPrompt);
39
+
40
+ // 4. Build raw report
41
+ const report = buildContextReport(sections, skills, techStack);
42
+
43
+ // 5. Show TUI
44
+ await showReport(platform, ctx, report);
45
+ })().catch((err) => {
46
+ ctx.ui.notify(`Optimize error: ${(err as Error).message}`, "error");
47
+ });
48
+ }
49
+
50
+ export function registerOptimizeContextCommand(platform: Platform): void {
51
+ platform.registerCommand("supi:optimize-context", {
52
+ description: "Analyze context usage and suggest token optimizations",
53
+ async handler(_args: string | undefined, ctx: any) {
54
+ handleOptimizeContext(platform, ctx);
55
+ },
56
+ });
57
+ }
58
+
59
+ // ── Internal ──────────────────────────────────────────────
60
+
61
+ async function showReport(
62
+ platform: Platform,
63
+ ctx: PlatformContext,
64
+ report: ContextReport,
65
+ ): Promise<void> {
66
+ const techList = [
67
+ ...report.techStack.languages,
68
+ ...(report.techStack.runtime ? [report.techStack.runtime] : []),
69
+ ...report.techStack.frameworks,
70
+ ...report.techStack.tools,
71
+ ].join(", ");
72
+
73
+ const lines: string[] = [
74
+ `Tech: ${techList || "unknown"}`,
75
+ `Current: ~${formatTokens(report.totalTokens)} tokens | Target: ~8.0K tokens`,
76
+ "",
77
+ ];
78
+
79
+ // Skills breakdown
80
+ if (report.skills.length === 0) {
81
+ lines.push("No skills detected in system prompt.");
82
+ } else {
83
+ const totalSkillTokens = report.skills.reduce((sum, s) => sum + s.tokens, 0);
84
+ lines.push(`Skills (${report.skills.length} loaded, ~${formatTokens(totalSkillTokens)} tok total):`);
85
+ lines.push("");
86
+
87
+ const sorted = [...report.skills].sort((a, b) => b.tokens - a.tokens);
88
+ for (const s of sorted) {
89
+ lines.push(` ${s.name.padEnd(32)} ~${formatTokens(s.tokens)} tok`);
90
+ }
91
+ }
92
+
93
+ // Non-skill sections
94
+ if (report.sections.length > 0) {
95
+ lines.push("");
96
+ lines.push("Other sections:");
97
+ lines.push("");
98
+
99
+ for (const sec of report.sections) {
100
+ const tok = formatTokens(sec.tokens);
101
+ const note = sec.note ? ` (${sec.note})` : "";
102
+ lines.push(` ${sec.label.padEnd(28)} ~${tok} tok${note}`);
103
+ }
104
+ }
105
+
106
+ const message = lines.join("\n");
107
+
108
+ // confirm() may not be available on all platforms
109
+ const shouldOptimize = ctx.ui.confirm
110
+ ? await ctx.ui.confirm("Context Optimization", message)
111
+ : (await ctx.ui.select("Context Optimization", [message, "▶ Optimize with AI", "Close"]))?.includes("Optimize");
112
+
113
+ if (!shouldOptimize) return;
114
+
115
+ platform.sendMessage(
116
+ {
117
+ customType: "optimize-context",
118
+ content: [{ type: "text", text: buildOptimizationPrompt(report) }],
119
+ display: "none",
120
+ },
121
+ { deliverAs: "steer", triggerTurn: true },
122
+ );
123
+ }
124
+
125
+ function buildOptimizationPrompt(report: ContextReport): string {
126
+ const lines: string[] = [];
127
+
128
+ const techList = [
129
+ ...report.techStack.languages,
130
+ ...(report.techStack.runtime ? [report.techStack.runtime] : []),
131
+ ...report.techStack.frameworks,
132
+ ...report.techStack.tools,
133
+ ].join(", ");
134
+
135
+ lines.push("# Context Optimization Request");
136
+ lines.push("");
137
+ lines.push(`Current system prompt is **~${formatTokens(report.totalTokens)} tokens**. Target is **< 8K tokens**.`);
138
+ lines.push(`Project tech stack: **${techList || "unknown"}**`);
139
+ lines.push("");
140
+
141
+ // Skill inventory
142
+ if (report.skills.length > 0) {
143
+ lines.push("## Skills currently loaded");
144
+ lines.push("");
145
+ lines.push("| Skill | Tokens |");
146
+ lines.push("|-------|--------|");
147
+ const sorted = [...report.skills].sort((a, b) => b.tokens - a.tokens);
148
+ for (const s of sorted) {
149
+ lines.push(`| ${s.name} | ~${formatTokens(s.tokens)} |`);
150
+ }
151
+ lines.push("");
152
+ }
153
+
154
+ // Section inventory
155
+ if (report.sections.length > 0) {
156
+ lines.push("## Other prompt sections");
157
+ lines.push("");
158
+ for (const sec of report.sections) {
159
+ const note = sec.note ? ` — ${sec.note}` : "";
160
+ lines.push(`- **${sec.label}**: ~${formatTokens(sec.tokens)} tok${note}`);
161
+ }
162
+ lines.push("");
163
+ }
164
+
165
+ lines.push("## Your task");
166
+ lines.push("");
167
+ lines.push("Classify each loaded skill into one of these actions:");
168
+ lines.push("");
169
+ lines.push("| Action | Prompt cost | When to use |");
170
+ lines.push("|--------|-------------|-------------|");
171
+ lines.push("| **Keep as skill** | Full content every turn | Essential for this project, needed constantly |");
172
+ lines.push("| **Convert to rulebook rule** | Name + description only | Relevant but only needed on-demand (load via `rule://`) |");
173
+ lines.push("| **Convert to TTSR rule** | Zero | Behavioral enforcement — triggered by regex pattern in output stream |");
174
+ lines.push("| **Convert to slash command** | Zero | Interactive workflow the user invokes explicitly |");
175
+ lines.push("| **Disable** | Zero | Irrelevant to this project's tech stack |");
176
+ lines.push("");
177
+ lines.push("For each skill, consider:");
178
+ lines.push("1. Is it relevant to the detected tech stack? If not → **disable**.");
179
+ lines.push("2. Does it enforce a behavior pattern (debugging, TDD, verification)? → **TTSR** with a condition regex.");
180
+ lines.push("3. Is it reference material loaded for occasional lookups? → **rulebook** with a short description.");
181
+ lines.push("4. Is it an interactive workflow the user triggers explicitly? → **slash command**.");
182
+ lines.push("5. Is it essential context needed on every turn for this project? → **keep**.");
183
+ lines.push("");
184
+ lines.push("## Implementation");
185
+ lines.push("");
186
+ lines.push("After classifying, implement the changes:");
187
+ lines.push("");
188
+ lines.push("- **Rulebook**: Create `.omp/rules/<skill-name>.md` with YAML frontmatter `description: \"...\"` and condensed key content");
189
+ lines.push("- **TTSR**: Create `.omp/rules/<skill-name>.md` with YAML frontmatter `condition: \"regex_pattern\"`");
190
+ lines.push("- **Disable**: Note which skills to remove from the session configuration");
191
+ lines.push("- **Command**: Note which skills could become slash commands (but don't create them now)");
192
+ lines.push("");
193
+ lines.push("## Warnings");
194
+ lines.push("");
195
+ lines.push("- Do **NOT** delete files from `~/.omp/skills/` — only create project-local `.omp/rules/` files");
196
+ lines.push("- Rulebook and TTSR files go in `.omp/rules/` at the project root");
197
+ lines.push("- Preserve the original skill content's intent when condensing for rulebook rules");
198
+ lines.push("");
199
+ lines.push("Present your classification table and implementation plan first, then ask before executing.");
200
+
201
+ return lines.join("\n");
202
+ }
@@ -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
  {
@@ -20,6 +20,7 @@ export function handleSupi(platform: Platform, ctx: PlatformContext): void {
20
20
  "/supi:doctor — Run health checks",
21
21
  "/supi:update — Update to latest version",
22
22
  "/supi:context — Show context breakdown",
23
+ "/supi:optimize-context — Optimize context to save tokens",
23
24
  ];
24
25
 
25
26
  const status = [