@kodrunhq/opencode-autopilot 1.1.2 → 1.2.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.
@@ -2,113 +2,26 @@
2
2
  description: Configure opencode-autopilot model assignments for each agent group
3
3
  agent: autopilot
4
4
  ---
5
- Help the user configure opencode-autopilot by walking through the model
6
- assignment process interactively.
5
+ Model configuration uses an interactive terminal wizard with searchable
6
+ model selection. This cannot be done inside the OpenCode TUI — it requires
7
+ a separate terminal.
7
8
 
8
- ## Step 1: Discover available models
9
+ Tell the user to open a terminal and run:
9
10
 
10
- Call the oc_configure tool with subcommand "start". This returns:
11
- - `availableModels`: a map of provider -> list of "provider/model" strings
12
- - `groups`: the 8 agent groups with descriptions and recommendations
13
- - `currentConfig`: existing assignments if reconfiguring
14
- - `diversityRules`: adversarial diversity constraints
15
-
16
- If `availableModels` is empty or has no entries, tell the user:
17
- "No models were discovered from your providers. Run `opencode models`
18
- in your terminal to see available models, then type them manually below."
19
-
20
- ## Step 2: Build the complete model list
21
-
22
- Combine ALL models from ALL providers into a single numbered list.
23
- Every model the user has access to must appear. Do NOT filter, summarize,
24
- or show only "recommended" models. The user decides — you present options.
25
-
26
- Example (show ALL of them, not a subset). The IDs must match exactly
27
- what `availableModels` returns (provider prefix comes from the provider):
28
11
  ```
29
- Available models:
30
- 1. anthropic/claude-opus-4-6
31
- 2. anthropic/claude-sonnet-4-6
32
- 3. anthropic/claude-haiku-4-5
33
- 4. openai/gpt-5.4
34
- 5. openai/gpt-5.4-mini
35
- 6. openai/gpt-5.4-codex
36
- 7. google/gemini-3.1-pro
37
- 8. google/gemini-3-flash
38
- ...
39
- ```
40
-
41
- ## Step 3: Walk through each group
42
-
43
- For each of the 8 groups (architects first, utilities last):
44
-
45
- 1. Explain what the group does and which agents belong to it
46
- 2. Show the tier recommendation
47
- 3. For adversarial groups (challengers, reviewers, red-team): explain WHY
48
- model diversity matters and which group they are adversarial to
49
- 4. Show the full numbered model list again whenever asking for selections
50
-
51
- ### Collecting models for each group
52
-
53
- For each group, collect an ORDERED LIST of models (not just one):
54
-
55
- ```
56
- Group: Architects
57
- Pick models in priority order. The first is the primary; the rest are
58
- fallbacks tried in sequence when the primary is rate-limited or fails.
59
-
60
- Enter model numbers separated by commas (e.g. 1,4,7):
12
+ bunx @kodrunhq/opencode-autopilot configure
61
13
  ```
62
14
 
63
- - The FIRST number is the primary model
64
- - All subsequent numbers are fallbacks, tried in the order given
65
- - Minimum 1 model (the primary), recommend 2-3 total
66
- - Emphasize that fallbacks are the core feature: "When your primary model
67
- hits a rate limit, the plugin automatically retries with the next model
68
- in your fallback chain. More fallbacks = more resilience."
69
-
70
- Parse the user's response:
71
- - If they send numbers like "1,4,7": map to model IDs
72
- - If they send model IDs directly: use as-is
73
- - If they send a single number: that's the primary with no fallbacks
74
-
75
- Call oc_configure with subcommand "assign":
76
- - `group`: the group ID
77
- - `primary`: first model from the user's list
78
- - `fallbacks`: remaining models as comma-separated string
79
-
80
- ### Diversity warnings
81
-
82
- If the assign response contains `diversityWarnings`, explain them
83
- conversationally. Strong warnings should be highlighted — the user can
84
- still proceed, but make the quality trade-off clear.
85
-
86
- Example: "Heads up: Architects and Challengers both use Claude models.
87
- Challengers are supposed to critique Architect decisions — using the same
88
- model family means you get confirmation bias instead of genuine challenge.
89
- Consider picking a different family for one of them. Continue anyway?"
90
-
91
- ## Step 4: Commit and verify
92
-
93
- After all 8 groups are assigned, call oc_configure with subcommand "commit".
94
-
95
- Then call oc_configure with subcommand "doctor" to verify health.
96
-
97
- Show a final summary table:
98
-
99
- ```
100
- Group | Primary | Fallbacks
101
- ───────────────┼──────────────────────────────┼──────────────────────────
102
- Architects | anthropic/claude-opus-4-6 | openai/gpt-5.4
103
- Challengers | openai/gpt-5.4 | google/gemini-3.1-pro
104
- ...
105
- ```
15
+ This will:
16
+ 1. Discover all available models from their configured providers
17
+ 2. Walk through each of the 8 agent groups with a searchable model picker
18
+ 3. For each group, select a primary model and optional fallback models
19
+ 4. Check adversarial diversity (different families for groups that review each other)
20
+ 5. Save the configuration
106
21
 
107
- ## Rules
22
+ After running the wizard, tell the user to restart OpenCode to pick up
23
+ the new model assignments.
108
24
 
109
- - NEVER pre-select models for the user. Always present the full list.
110
- - NEVER skip fallback collection. Always ask for ordered model lists.
111
- - NEVER filter the model list to "recommended" models. Show everything.
112
- - If the user says "pick for me" or "use defaults", THEN you may suggest
113
- assignments based on the tier recommendations and diversity rules, but
114
- still show what you picked and ask for confirmation.
25
+ Do NOT attempt to configure models through the oc_configure tool's assign
26
+ subcommand directly. The interactive wizard is the supported configuration
27
+ method.
package/bin/cli.ts CHANGED
@@ -10,6 +10,7 @@ import { diagnose } from "../src/registry/doctor";
10
10
  import { ALL_GROUP_IDS, DIVERSITY_RULES, GROUP_DEFINITIONS } from "../src/registry/model-groups";
11
11
  import type { GroupId } from "../src/registry/types";
12
12
  import { fileExists } from "../src/utils/fs-helpers";
13
+ import { runConfigure } from "./configure-tui";
13
14
 
14
15
  const execFile = promisify(execFileCb);
15
16
 
@@ -124,14 +125,12 @@ export async function runInstall(options: CliOptions = {}): Promise<void> {
124
125
  console.log("");
125
126
  console.log(bold("Next steps:"));
126
127
  console.log("");
127
- console.log(" 1. Launch OpenCode");
128
- console.log(" 2. Run /oc-configure to set up your model assignments");
128
+ console.log(" Run the interactive configuration wizard:");
129
129
  console.log("");
130
- console.log(" Or paste this into your AI session for guided setup:");
130
+ console.log(` ${bold("bunx @kodrunhq/opencode-autopilot configure")}`);
131
131
  console.log("");
132
- console.log(
133
- " https://raw.githubusercontent.com/kodrunhq/opencode-autopilot/main/docs/guide/installation.md",
134
- );
132
+ console.log(" This walks through each agent group with searchable model");
133
+ console.log(" selection and fallback configuration.");
135
134
  console.log("");
136
135
  }
137
136
 
@@ -313,6 +312,7 @@ function printUsage(): void {
313
312
  console.log("");
314
313
  console.log("Commands:");
315
314
  console.log(" install Register the plugin and create starter config");
315
+ console.log(" configure Interactive model assignment for each agent group");
316
316
  console.log(" doctor Check installation health and model assignments");
317
317
  console.log("");
318
318
  console.log("Options:");
@@ -330,6 +330,9 @@ if (import.meta.main) {
330
330
  case "install":
331
331
  await runInstall({ noTui: args.includes("--no-tui") });
332
332
  break;
333
+ case "configure":
334
+ await runConfigure();
335
+ break;
333
336
  case "doctor":
334
337
  await runDoctor();
335
338
  break;
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Interactive TUI for configuring model assignments.
3
+ * Uses @inquirer/search (filterable single-select) and @inquirer/checkbox
4
+ * (multi-select) to handle 100+ models deterministically — no LLM involved.
5
+ */
6
+
7
+ import { execFile as execFileCb } from "node:child_process";
8
+ import { promisify } from "node:util";
9
+ import checkbox, { Separator as CheckboxSeparator } from "@inquirer/checkbox";
10
+ import confirm from "@inquirer/confirm";
11
+ import search from "@inquirer/search";
12
+ import { CONFIG_PATH, createDefaultConfig, loadConfig, saveConfig } from "../src/config";
13
+ import { checkDiversity } from "../src/registry/diversity";
14
+ import { ALL_GROUP_IDS, DIVERSITY_RULES, GROUP_DEFINITIONS } from "../src/registry/model-groups";
15
+ import type { GroupId, GroupModelAssignment } from "../src/registry/types";
16
+
17
+ const execFile = promisify(execFileCb);
18
+
19
+ // ── ANSI helpers ───────────────────────────────────────────────────
20
+
21
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
22
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
23
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
24
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
25
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
26
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
27
+
28
+ // ── Model discovery ────────────────────────────────────────────────
29
+
30
+ interface DiscoveredModel {
31
+ readonly id: string; // "provider/model"
32
+ readonly provider: string;
33
+ readonly model: string;
34
+ }
35
+
36
+ /**
37
+ * Discover available models by running `opencode models`.
38
+ * Each line of output is a "provider/model" string.
39
+ */
40
+ async function discoverModels(): Promise<readonly DiscoveredModel[]> {
41
+ try {
42
+ const { stdout } = await execFile("opencode", ["models"]);
43
+ const lines = stdout
44
+ .split("\n")
45
+ .map((l) => l.trim())
46
+ .filter(Boolean);
47
+
48
+ return lines.map((id) => {
49
+ const slashIndex = id.indexOf("/");
50
+ return {
51
+ id,
52
+ provider: slashIndex > 0 ? id.slice(0, slashIndex) : "unknown",
53
+ model: slashIndex > 0 ? id.slice(slashIndex + 1) : id,
54
+ };
55
+ });
56
+ } catch {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Group models by provider for display with separators.
63
+ */
64
+ function groupByProvider(models: readonly DiscoveredModel[]): Map<string, DiscoveredModel[]> {
65
+ const grouped = new Map<string, DiscoveredModel[]>();
66
+ for (const m of models) {
67
+ const existing = grouped.get(m.provider) ?? [];
68
+ existing.push(m);
69
+ grouped.set(m.provider, existing);
70
+ }
71
+ return grouped;
72
+ }
73
+
74
+ // ── Search source for @inquirer/search ─────────────────────────────
75
+
76
+ function createSearchSource(models: readonly DiscoveredModel[], exclude?: Set<string>) {
77
+ const byProvider = groupByProvider(models);
78
+
79
+ return async (term: string | undefined) => {
80
+ const results: Array<
81
+ { name: string; value: string; description: string } | typeof CheckboxSeparator.prototype
82
+ > = [];
83
+
84
+ for (const [provider, providerModels] of byProvider) {
85
+ const filtered = providerModels.filter((m) => {
86
+ if (exclude?.has(m.id)) return false;
87
+ if (!term) return true;
88
+ return m.id.toLowerCase().includes(term.toLowerCase());
89
+ });
90
+
91
+ if (filtered.length === 0) continue;
92
+
93
+ results.push(new CheckboxSeparator(`── ${provider} ──`));
94
+ for (const m of filtered) {
95
+ results.push({
96
+ name: m.id,
97
+ value: m.id,
98
+ description: m.model,
99
+ });
100
+ }
101
+ }
102
+
103
+ return results;
104
+ };
105
+ }
106
+
107
+ // ── Checkbox choices for fallback selection ─────────────────────────
108
+
109
+ function createCheckboxChoices(models: readonly DiscoveredModel[], excludePrimary: string) {
110
+ const byProvider = groupByProvider(models);
111
+ const choices: Array<{ name: string; value: string } | InstanceType<typeof CheckboxSeparator>> =
112
+ [];
113
+
114
+ for (const [provider, providerModels] of byProvider) {
115
+ const filtered = providerModels.filter((m) => m.id !== excludePrimary);
116
+ if (filtered.length === 0) continue;
117
+
118
+ choices.push(new CheckboxSeparator(`── ${provider} ──`));
119
+ for (const m of filtered) {
120
+ choices.push({
121
+ name: m.id,
122
+ value: m.id,
123
+ });
124
+ }
125
+ }
126
+
127
+ return choices;
128
+ }
129
+
130
+ // ── Diversity check display ────────────────────────────────────────
131
+
132
+ function extractFamily(model: string): string {
133
+ const slashIndex = model.indexOf("/");
134
+ return slashIndex > 0 ? model.slice(0, slashIndex) : model;
135
+ }
136
+
137
+ function showDiversityWarnings(assignments: Record<string, GroupModelAssignment>): void {
138
+ const warnings = checkDiversity(assignments);
139
+ if (warnings.length === 0) return;
140
+
141
+ console.log("");
142
+ for (const w of warnings) {
143
+ const groupLabels = w.groups.map((g) => GROUP_DEFINITIONS[g as GroupId]?.label ?? g);
144
+ const label = groupLabels.join(" & ");
145
+ const severity = w.rule.severity === "strong" ? red("WARNING") : yellow("note");
146
+ console.log(` ${severity}: ${label} both use ${cyan(w.sharedFamily)} family`);
147
+ console.log(` ${dim(w.rule.reason)}`);
148
+ console.log("");
149
+ }
150
+ }
151
+
152
+ // ── Group walkthrough ──────────────────────────────────────────────
153
+
154
+ async function configureGroup(
155
+ groupId: GroupId,
156
+ models: readonly DiscoveredModel[],
157
+ assignments: Record<string, GroupModelAssignment>,
158
+ ): Promise<GroupModelAssignment> {
159
+ const def = GROUP_DEFINITIONS[groupId];
160
+
161
+ console.log("");
162
+ console.log(bold(`── ${def.label} ──────────────────────────────────────`));
163
+ console.log(` ${dim("Purpose:")} ${def.purpose}`);
164
+ console.log(` ${dim("Recommendation:")} ${def.recommendation}`);
165
+
166
+ // Check if this group is adversarial to another
167
+ for (const rule of DIVERSITY_RULES) {
168
+ if (rule.groups.includes(groupId)) {
169
+ const others = rule.groups.filter((g) => g !== groupId);
170
+ const assignedOthers = others.filter((g) => assignments[g]);
171
+ if (assignedOthers.length > 0) {
172
+ const otherLabels = assignedOthers.map(
173
+ (g) =>
174
+ `${GROUP_DEFINITIONS[g as GroupId].label} (${extractFamily(assignments[g].primary)})`,
175
+ );
176
+ console.log(
177
+ ` ${yellow("⚡")} Adversarial to: ${otherLabels.join(", ")} — pick a ${bold("different")} family`,
178
+ );
179
+ }
180
+ }
181
+ }
182
+
183
+ console.log("");
184
+
185
+ // Primary model — searchable select
186
+ const primary = await search({
187
+ message: `Primary model for ${def.label}:`,
188
+ source: createSearchSource(models),
189
+ pageSize: 15,
190
+ });
191
+
192
+ // Fallback models — checkbox multi-select
193
+ const wantFallbacks = await confirm({
194
+ message: "Add fallback models? (recommended for resilience)",
195
+ default: true,
196
+ });
197
+
198
+ let fallbacks: string[] = [];
199
+ if (wantFallbacks) {
200
+ fallbacks = await checkbox({
201
+ message: `Fallback models for ${def.label} (space to select, enter to confirm):`,
202
+ choices: createCheckboxChoices(models, primary),
203
+ pageSize: 15,
204
+ });
205
+ }
206
+
207
+ const assignment: GroupModelAssignment = Object.freeze({
208
+ primary,
209
+ fallbacks: Object.freeze(fallbacks),
210
+ });
211
+
212
+ // Show what was selected
213
+ console.log(
214
+ ` ${green("✓")} ${def.label}: ${cyan(primary)}${fallbacks.length > 0 ? ` → ${fallbacks.map(cyan).join(" → ")}` : ""}`,
215
+ );
216
+
217
+ return assignment;
218
+ }
219
+
220
+ // ── Main configure flow ────────────────────────────────────────────
221
+
222
+ export async function runConfigure(configPath: string = CONFIG_PATH): Promise<void> {
223
+ console.log("");
224
+ console.log(bold("opencode-autopilot configure"));
225
+ console.log("────────────────────────────");
226
+ console.log("");
227
+
228
+ // 1. Discover models
229
+ console.log(" Discovering available models...");
230
+ const models = await discoverModels();
231
+
232
+ if (models.length === 0) {
233
+ console.log("");
234
+ console.log(red(" No models found."));
235
+ console.log(" Make sure OpenCode is installed and you have providers configured.");
236
+ console.log(" Run: opencode providers list");
237
+ console.log("");
238
+ process.exit(1);
239
+ }
240
+
241
+ const byProvider = groupByProvider(models);
242
+ console.log(
243
+ ` ${green("✓")} Found ${bold(String(models.length))} models from ${bold(String(byProvider.size))} providers`,
244
+ );
245
+
246
+ // 2. Load existing config
247
+ const existingConfig = await loadConfig(configPath);
248
+ if (existingConfig?.configured) {
249
+ console.log(` ${yellow("⚠")} Existing configuration found — this will overwrite it`);
250
+ const proceed = await confirm({
251
+ message: "Continue with reconfiguration?",
252
+ default: true,
253
+ });
254
+ if (!proceed) {
255
+ console.log(" Cancelled.");
256
+ return;
257
+ }
258
+ }
259
+
260
+ console.log("");
261
+ console.log(bold("Walk through each agent group and assign models."));
262
+ console.log(dim("Type to search, arrow keys to navigate, enter to select."));
263
+ console.log(dim("For fallbacks: space to toggle, enter to confirm selection."));
264
+
265
+ // 3. Walk through each group
266
+ const assignments: Record<string, GroupModelAssignment> = {};
267
+
268
+ for (const groupId of ALL_GROUP_IDS) {
269
+ assignments[groupId] = await configureGroup(groupId, models, assignments);
270
+ showDiversityWarnings(assignments);
271
+ }
272
+
273
+ // 4. Show summary
274
+ console.log("");
275
+ console.log(bold("── Summary ───────────────────────────────────────────"));
276
+ console.log("");
277
+
278
+ const labelWidth = Math.max(...ALL_GROUP_IDS.map((id) => GROUP_DEFINITIONS[id].label.length)) + 2;
279
+
280
+ for (const groupId of ALL_GROUP_IDS) {
281
+ const def = GROUP_DEFINITIONS[groupId];
282
+ const a = assignments[groupId];
283
+ const label = def.label.padEnd(labelWidth);
284
+ const fallbackStr = a.fallbacks.length > 0 ? ` → ${a.fallbacks.join(" → ")}` : "";
285
+ console.log(` ${label} ${cyan(a.primary)}${dim(fallbackStr)}`);
286
+ }
287
+
288
+ console.log("");
289
+
290
+ // 5. Confirm and save
291
+ const doCommit = await confirm({
292
+ message: "Save this configuration?",
293
+ default: true,
294
+ });
295
+
296
+ if (!doCommit) {
297
+ console.log(" Configuration discarded.");
298
+ return;
299
+ }
300
+
301
+ // Build and save config
302
+ const baseConfig = existingConfig ?? createDefaultConfig();
303
+ const groupsRecord: Record<string, { primary: string; fallbacks: string[] }> = {};
304
+ for (const [key, val] of Object.entries(assignments)) {
305
+ groupsRecord[key] = { primary: val.primary, fallbacks: [...val.fallbacks] };
306
+ }
307
+
308
+ const newConfig = {
309
+ ...baseConfig,
310
+ version: 4 as const,
311
+ configured: true,
312
+ groups: groupsRecord,
313
+ overrides: baseConfig.overrides ?? {},
314
+ };
315
+
316
+ await saveConfig(newConfig, configPath);
317
+ console.log(` ${green("✓")} Configuration saved`);
318
+
319
+ // 6. Final diversity check
320
+ console.log("");
321
+ console.log(bold("Adversarial Diversity Check"));
322
+ const finalWarnings = checkDiversity(groupsRecord);
323
+ if (finalWarnings.length === 0) {
324
+ console.log(` ${green("✓")} All adversarial groups use different model families`);
325
+ } else {
326
+ for (const w of finalWarnings) {
327
+ const groupLabels = w.groups.map((g) => GROUP_DEFINITIONS[g as GroupId]?.label ?? g);
328
+ console.log(
329
+ ` ${yellow("⚠")} ${groupLabels.join(" & ")}: shared ${cyan(w.sharedFamily)} family — ${dim(w.rule.reason)}`,
330
+ );
331
+ }
332
+ }
333
+
334
+ console.log("");
335
+ console.log(green("Configuration complete!"));
336
+ console.log(dim("Run 'bunx @kodrunhq/opencode-autopilot doctor' to verify."));
337
+ console.log("");
338
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodrunhq/opencode-autopilot",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Curated agents, skills, and commands for the OpenCode AI coding CLI — autonomous orchestrator, multi-agent code review, model fallback, and in-session asset creation tools.",
5
5
  "main": "src/index.ts",
6
6
  "keywords": [
@@ -51,6 +51,10 @@
51
51
  },
52
52
  "type": "module",
53
53
  "dependencies": {
54
+ "@inquirer/checkbox": "^5.1.2",
55
+ "@inquirer/confirm": "^6.0.10",
56
+ "@inquirer/search": "^4.1.6",
57
+ "@inquirer/select": "^5.1.2",
54
58
  "yaml": "^2.8.3"
55
59
  }
56
60
  }
package/src/installer.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { readdir, unlink } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { copyIfMissing, isEnoentError } from "./utils/fs-helpers";
1
+ import { access, copyFile, readdir, unlink } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { copyIfMissing, ensureDir, isEnoentError } from "./utils/fs-helpers";
4
4
  import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
5
5
 
6
6
  /**
@@ -9,6 +9,13 @@ import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
9
9
  */
10
10
  const DEPRECATED_ASSETS = ["agents/placeholder-agent.md", "commands/configure.md"] as const;
11
11
 
12
+ /**
13
+ * Assets that must be overwritten on every install, even if the user has a copy.
14
+ * Used when the shipped version has critical fixes that override user customizations.
15
+ * Remove entries once the fix has been deployed long enough.
16
+ */
17
+ const FORCE_UPDATE_ASSETS = ["commands/oc-configure.md"] as const;
18
+
12
19
  export interface InstallResult {
13
20
  readonly copied: readonly string[];
14
21
  readonly skipped: readonly string[];
@@ -138,6 +145,33 @@ async function cleanupDeprecatedAssets(
138
145
  return { removed, errors };
139
146
  }
140
147
 
148
+ async function forceUpdateAssets(
149
+ sourceDir: string,
150
+ targetDir: string,
151
+ ): Promise<{ readonly updated: readonly string[]; readonly errors: readonly string[] }> {
152
+ const updated: string[] = [];
153
+ const errors: string[] = [];
154
+ for (const asset of FORCE_UPDATE_ASSETS) {
155
+ const source = join(sourceDir, asset);
156
+ const target = join(targetDir, asset);
157
+ try {
158
+ // Verify source exists before attempting copy — skip silently if
159
+ // the source isn't in the bundle (test environments, partial installs)
160
+ await access(source);
161
+ await ensureDir(dirname(target));
162
+ await copyFile(source, target);
163
+ updated.push(asset);
164
+ } catch (error: unknown) {
165
+ if (!isEnoentError(error)) {
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ errors.push(`force-update ${asset}: ${message}`);
168
+ }
169
+ // ENOENT = source not in bundle, skip silently
170
+ }
171
+ }
172
+ return { updated, errors };
173
+ }
174
+
141
175
  export async function installAssets(
142
176
  assetsDir: string = getAssetsDir(),
143
177
  targetDir: string = getGlobalConfigDir(),
@@ -145,6 +179,9 @@ export async function installAssets(
145
179
  // Remove deprecated assets before copying new ones
146
180
  const cleanup = await cleanupDeprecatedAssets(targetDir);
147
181
 
182
+ // Force-overwrite assets with critical fixes
183
+ const forceUpdate = await forceUpdateAssets(assetsDir, targetDir);
184
+
148
185
  const [agents, commands, skills] = await Promise.all([
149
186
  processFiles(assetsDir, targetDir, "agents"),
150
187
  processFiles(assetsDir, targetDir, "commands"),
@@ -152,8 +189,14 @@ export async function installAssets(
152
189
  ]);
153
190
 
154
191
  return {
155
- copied: [...agents.copied, ...commands.copied, ...skills.copied],
192
+ copied: [...forceUpdate.updated, ...agents.copied, ...commands.copied, ...skills.copied],
156
193
  skipped: [...agents.skipped, ...commands.skipped, ...skills.skipped],
157
- errors: [...cleanup.errors, ...agents.errors, ...commands.errors, ...skills.errors],
194
+ errors: [
195
+ ...cleanup.errors,
196
+ ...forceUpdate.errors,
197
+ ...agents.errors,
198
+ ...commands.errors,
199
+ ...skills.errors,
200
+ ],
158
201
  };
159
202
  }
@@ -117,6 +117,37 @@ function serializeDiversityWarnings(warnings: readonly DiversityWarning[]): read
117
117
  }));
118
118
  }
119
119
 
120
+ /**
121
+ * Build a flat numbered list of all available models and an index map.
122
+ * Returns { numberedList: "1. provider/model\n2. ...", indexMap: { "1": "provider/model", ... } }
123
+ */
124
+ function buildNumberedModelList(modelsByProvider: Map<string, string[]>): {
125
+ numberedList: string;
126
+ indexMap: Record<string, string>;
127
+ totalCount: number;
128
+ } {
129
+ const allModels: string[] = [];
130
+ for (const models of modelsByProvider.values()) {
131
+ allModels.push(...models);
132
+ }
133
+ // Sort alphabetically for stable ordering
134
+ allModels.sort();
135
+
136
+ const indexMap: Record<string, string> = {};
137
+ const lines: string[] = [];
138
+ for (let i = 0; i < allModels.length; i++) {
139
+ const num = String(i + 1);
140
+ indexMap[num] = allModels[i];
141
+ lines.push(` ${num}. ${allModels[i]}`);
142
+ }
143
+
144
+ return {
145
+ numberedList: lines.join("\n"),
146
+ indexMap,
147
+ totalCount: allModels.length,
148
+ };
149
+ }
150
+
120
151
  async function handleStart(configPath?: string): Promise<string> {
121
152
  // Wait for background provider discovery (up to 5s) before building model list
122
153
  await Promise.race([
@@ -125,6 +156,7 @@ async function handleStart(configPath?: string): Promise<string> {
125
156
  ]);
126
157
 
127
158
  const modelsByProvider = discoverAvailableModels();
159
+ const { numberedList, indexMap, totalCount } = buildNumberedModelList(modelsByProvider);
128
160
 
129
161
  // Load current plugin config to show existing assignments
130
162
  const currentConfig = await loadConfig(configPath);
@@ -148,15 +180,53 @@ async function handleStart(configPath?: string): Promise<string> {
148
180
  };
149
181
  });
150
182
 
183
+ // Pre-formatted text the LLM should show verbatim — avoids summarization
184
+ const displayText =
185
+ totalCount > 0
186
+ ? [
187
+ `Available models (${totalCount} total):`,
188
+ numberedList,
189
+ "",
190
+ "For each group below, enter model numbers separated by commas (e.g. 1,4,7).",
191
+ "First number = primary model. Remaining = fallbacks tried in order.",
192
+ "More fallbacks = more resilience when a model is rate-limited.",
193
+ ].join("\n")
194
+ : [
195
+ "No models were discovered from your providers.",
196
+ "Run `opencode models` in your terminal to see available models,",
197
+ "then type model IDs manually (e.g. anthropic/claude-opus-4-6).",
198
+ ].join("\n");
199
+
200
+ // Compact group summaries — only fields the LLM needs for the walkthrough
201
+ const compactGroups = groups.map((g) => ({
202
+ id: g.id,
203
+ label: g.label,
204
+ purpose: g.purpose,
205
+ recommendation: g.recommendation,
206
+ agents: g.agents,
207
+ currentAssignment: g.currentAssignment,
208
+ }));
209
+
210
+ // Compact diversity rules — just the text the LLM should mention
211
+ const compactRules = DIVERSITY_RULES.map((r) => ({
212
+ groups: r.groups,
213
+ severity: r.severity,
214
+ reason: r.reason,
215
+ }));
216
+
217
+ // NOTE: availableModels is intentionally excluded — it's redundant with
218
+ // displayText/modelIndex and can be 400KB+ with many providers, causing
219
+ // OpenCode to truncate the tool output and lose everything after it.
151
220
  return JSON.stringify({
152
221
  action: "configure",
153
222
  stage: "start",
154
- availableModels: Object.fromEntries(modelsByProvider),
155
- groups,
223
+ displayText,
224
+ modelIndex: indexMap,
225
+ groups: compactGroups,
226
+ diversityRules: compactRules,
156
227
  currentConfig: currentConfig
157
228
  ? { configured: currentConfig.configured, groups: currentConfig.groups }
158
229
  : null,
159
- diversityRules: DIVERSITY_RULES,
160
230
  });
161
231
  }
162
232