@kodrunhq/opencode-autopilot 1.1.3 → 1.2.1
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/assets/commands/oc-configure.md +16 -77
- package/bin/cli.ts +9 -6
- package/bin/configure-tui.ts +338 -0
- package/package.json +5 -1
- package/src/installer.ts +48 -5
- package/src/tools/configure.ts +23 -4
|
@@ -2,87 +2,26 @@
|
|
|
2
2
|
description: Configure opencode-autopilot model assignments for each agent group
|
|
3
3
|
agent: autopilot
|
|
4
4
|
---
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
Call the oc_configure tool with subcommand "start". The response contains:
|
|
11
|
-
- `displayText`: a pre-formatted numbered list of ALL available models.
|
|
12
|
-
**Show this to the user VERBATIM. Do not summarize, truncate, or reformat it.**
|
|
13
|
-
- `modelIndex`: a map of number -> model ID (e.g. {"1": "anthropic/claude-opus-4-6"})
|
|
14
|
-
- `groups`: the 8 agent groups with descriptions and recommendations
|
|
15
|
-
- `currentConfig`: existing assignments if reconfiguring
|
|
16
|
-
- `diversityRules`: adversarial diversity constraints
|
|
17
|
-
|
|
18
|
-
Print `displayText` exactly as returned. This is the complete model list
|
|
19
|
-
with instructions. Do not add, remove, or reorder entries.
|
|
20
|
-
|
|
21
|
-
## Step 2: Walk through each group
|
|
22
|
-
|
|
23
|
-
For each of the 8 groups (architects first, utilities last):
|
|
24
|
-
|
|
25
|
-
1. Explain what the group does and which agents belong to it
|
|
26
|
-
2. Show the tier recommendation
|
|
27
|
-
3. For adversarial groups (challengers, reviewers, red-team): explain WHY
|
|
28
|
-
model diversity matters and which group they are adversarial to
|
|
29
|
-
4. Re-print `displayText` so the user can see the numbered list
|
|
30
|
-
|
|
31
|
-
Then ask:
|
|
9
|
+
Tell the user to open a terminal and run:
|
|
32
10
|
|
|
33
11
|
```
|
|
34
|
-
|
|
35
|
-
First = primary, rest = fallbacks in order.
|
|
12
|
+
bunx @kodrunhq/opencode-autopilot configure
|
|
36
13
|
```
|
|
37
14
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
The FIRST model is the primary. All subsequent models are fallbacks,
|
|
45
|
-
tried in sequence when the primary is rate-limited or fails.
|
|
46
|
-
|
|
47
|
-
Call oc_configure with subcommand "assign":
|
|
48
|
-
- `group`: the group ID
|
|
49
|
-
- `primary`: first model from the user's list
|
|
50
|
-
- `fallbacks`: remaining models as comma-separated string
|
|
51
|
-
|
|
52
|
-
### Diversity warnings
|
|
53
|
-
|
|
54
|
-
If the assign response contains `diversityWarnings`, explain them
|
|
55
|
-
conversationally. Strong warnings should be highlighted — the user can
|
|
56
|
-
still proceed, but make the quality trade-off clear.
|
|
57
|
-
|
|
58
|
-
Example: "Heads up: Architects and Challengers both use Claude models.
|
|
59
|
-
Challengers are supposed to critique Architect decisions — using the same
|
|
60
|
-
model family means you get confirmation bias instead of genuine challenge.
|
|
61
|
-
Consider picking a different family for one of them. Continue anyway?"
|
|
62
|
-
|
|
63
|
-
## Step 3: Commit and verify
|
|
64
|
-
|
|
65
|
-
After all 8 groups are assigned, call oc_configure with subcommand "commit".
|
|
66
|
-
|
|
67
|
-
Then call oc_configure with subcommand "doctor" to verify health.
|
|
68
|
-
|
|
69
|
-
Show a final summary table:
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
Group | Primary | Fallbacks
|
|
73
|
-
───────────────┼──────────────────────────────┼──────────────────────────
|
|
74
|
-
Architects | anthropic/claude-opus-4-6 | openai/gpt-5.4
|
|
75
|
-
Challengers | openai/gpt-5.4 | google/gemini-3.1-pro
|
|
76
|
-
...
|
|
77
|
-
```
|
|
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
|
|
78
21
|
|
|
79
|
-
|
|
22
|
+
After running the wizard, tell the user to restart OpenCode to pick up
|
|
23
|
+
the new model assignments.
|
|
80
24
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- NEVER pre-select models for the user. They choose from the full list.
|
|
85
|
-
- NEVER skip fallback collection. Emphasize: more fallbacks = more resilience.
|
|
86
|
-
- If the user says "pick for me" or "use defaults", THEN you may suggest
|
|
87
|
-
assignments based on the tier recommendations and diversity rules, but
|
|
88
|
-
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("
|
|
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("
|
|
130
|
+
console.log(` ${bold("bunx @kodrunhq/opencode-autopilot configure")}`);
|
|
131
131
|
console.log("");
|
|
132
|
-
console.log(
|
|
133
|
-
|
|
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
|
|
3
|
+
"version": "1.2.1",
|
|
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: [
|
|
194
|
+
errors: [
|
|
195
|
+
...cleanup.errors,
|
|
196
|
+
...forceUpdate.errors,
|
|
197
|
+
...agents.errors,
|
|
198
|
+
...commands.errors,
|
|
199
|
+
...skills.errors,
|
|
200
|
+
],
|
|
158
201
|
};
|
|
159
202
|
}
|
package/src/tools/configure.ts
CHANGED
|
@@ -197,17 +197,36 @@ async function handleStart(configPath?: string): Promise<string> {
|
|
|
197
197
|
"then type model IDs manually (e.g. anthropic/claude-opus-4-6).",
|
|
198
198
|
].join("\n");
|
|
199
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.
|
|
200
220
|
return JSON.stringify({
|
|
201
221
|
action: "configure",
|
|
202
222
|
stage: "start",
|
|
203
|
-
availableModels: Object.fromEntries(modelsByProvider),
|
|
204
|
-
modelIndex: indexMap,
|
|
205
223
|
displayText,
|
|
206
|
-
|
|
224
|
+
modelIndex: indexMap,
|
|
225
|
+
groups: compactGroups,
|
|
226
|
+
diversityRules: compactRules,
|
|
207
227
|
currentConfig: currentConfig
|
|
208
228
|
? { configured: currentConfig.configured, groups: currentConfig.groups }
|
|
209
229
|
: null,
|
|
210
|
-
diversityRules: DIVERSITY_RULES,
|
|
211
230
|
});
|
|
212
231
|
}
|
|
213
232
|
|