opencode-auto-agent 1.0.0 → 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.
package/src/lib/ui.js ADDED
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Terminal UI utilities — zero-dep, inspired by OpenCode's TUI aesthetic.
3
+ *
4
+ * Provides box-drawing, ANSI colors, spinners, and interactive prompts
5
+ * that work on Node 18+ with raw stdin.
6
+ */
7
+
8
+ import { createInterface } from "node:readline";
9
+ import { stdin, stdout } from "node:process";
10
+
11
+ // ── ANSI color helpers ─────────────────────────────────────────────────────
12
+
13
+ const ESC = "\x1b[";
14
+
15
+ export const c = {
16
+ reset: (s) => `${ESC}0m${s}${ESC}0m`,
17
+ bold: (s) => `${ESC}1m${s}${ESC}0m`,
18
+ dim: (s) => `${ESC}2m${s}${ESC}0m`,
19
+ italic: (s) => `${ESC}3m${s}${ESC}0m`,
20
+ underline:(s) => `${ESC}4m${s}${ESC}0m`,
21
+
22
+ // Theme colors (OpenCode-inspired)
23
+ cyan: (s) => `${ESC}36m${s}${ESC}0m`,
24
+ green: (s) => `${ESC}32m${s}${ESC}0m`,
25
+ yellow: (s) => `${ESC}33m${s}${ESC}0m`,
26
+ red: (s) => `${ESC}31m${s}${ESC}0m`,
27
+ magenta: (s) => `${ESC}35m${s}${ESC}0m`,
28
+ blue: (s) => `${ESC}34m${s}${ESC}0m`,
29
+ white: (s) => `${ESC}37m${s}${ESC}0m`,
30
+ gray: (s) => `${ESC}90m${s}${ESC}0m`,
31
+
32
+ // Bright variants
33
+ brightCyan: (s) => `${ESC}96m${s}${ESC}0m`,
34
+ brightGreen: (s) => `${ESC}92m${s}${ESC}0m`,
35
+ brightYellow: (s) => `${ESC}93m${s}${ESC}0m`,
36
+ brightRed: (s) => `${ESC}91m${s}${ESC}0m`,
37
+ brightMagenta: (s) => `${ESC}95m${s}${ESC}0m`,
38
+
39
+ // Background
40
+ bgCyan: (s) => `${ESC}46m${ESC}30m${s}${ESC}0m`,
41
+ bgGreen: (s) => `${ESC}42m${ESC}30m${s}${ESC}0m`,
42
+ bgYellow: (s) => `${ESC}43m${ESC}30m${s}${ESC}0m`,
43
+ bgRed: (s) => `${ESC}41m${ESC}37m${s}${ESC}0m`,
44
+ bgBlue: (s) => `${ESC}44m${ESC}37m${s}${ESC}0m`,
45
+ bgMagenta: (s) => `${ESC}45m${ESC}37m${s}${ESC}0m`,
46
+ };
47
+
48
+ // ── Box-drawing characters ─────────────────────────────────────────────────
49
+
50
+ const BOX = {
51
+ tl: "\u256d", tr: "\u256e", bl: "\u2570", br: "\u256f",
52
+ h: "\u2500", v: "\u2502",
53
+ cross: "\u253c",
54
+ tee_r: "\u251c", tee_l: "\u2524",
55
+ };
56
+
57
+ // ── Layout helpers ─────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Get terminal width, fallback to 80.
61
+ */
62
+ function termWidth() {
63
+ return stdout.columns || 80;
64
+ }
65
+
66
+ /**
67
+ * Draw a horizontal rule.
68
+ */
69
+ export function hr(char = BOX.h, width) {
70
+ const w = width || termWidth();
71
+ console.log(c.gray(char.repeat(w)));
72
+ }
73
+
74
+ /**
75
+ * Print a styled header banner (OpenCode-style).
76
+ */
77
+ export function banner(title, subtitle) {
78
+ const w = Math.min(termWidth(), 72);
79
+ const pad = (s, len) => {
80
+ const stripped = stripAnsi(s);
81
+ return s + " ".repeat(Math.max(0, len - stripped.length));
82
+ };
83
+
84
+ console.log();
85
+ console.log(c.cyan(` ${BOX.tl}${BOX.h.repeat(w - 4)}${BOX.tr}`));
86
+ console.log(c.cyan(` ${BOX.v}`) + " " + c.bold(c.brightCyan(pad(title, w - 6))) + c.cyan(BOX.v));
87
+ if (subtitle) {
88
+ console.log(c.cyan(` ${BOX.v}`) + " " + c.gray(pad(subtitle, w - 6)) + c.cyan(BOX.v));
89
+ }
90
+ console.log(c.cyan(` ${BOX.bl}${BOX.h.repeat(w - 4)}${BOX.br}`));
91
+ console.log();
92
+ }
93
+
94
+ /**
95
+ * Print a section header.
96
+ */
97
+ export function section(title) {
98
+ console.log();
99
+ console.log(` ${c.bold(c.cyan(title))}`);
100
+ console.log(` ${c.gray(BOX.h.repeat(stripAnsi(title).length + 2))}`);
101
+ }
102
+
103
+ /**
104
+ * Print a status line with icon.
105
+ */
106
+ export function status(icon, label, detail) {
107
+ const icons = {
108
+ pass: c.green("\u2713"),
109
+ fail: c.red("\u2717"),
110
+ warn: c.yellow("\u26A0"),
111
+ info: c.cyan("\u2139"),
112
+ arrow: c.cyan("\u25B6"),
113
+ dot: c.gray("\u2022"),
114
+ star: c.yellow("\u2605"),
115
+ };
116
+ const i = icons[icon] || icon;
117
+ const d = detail ? c.gray(` ${detail}`) : "";
118
+ console.log(` ${i} ${label}${d}`);
119
+ }
120
+
121
+ /**
122
+ * Print a key-value pair.
123
+ */
124
+ export function kv(key, value) {
125
+ console.log(` ${c.gray(key + ":")} ${c.white(String(value))}`);
126
+ }
127
+
128
+ /**
129
+ * Print an error block.
130
+ */
131
+ export function error(message, hint) {
132
+ console.log();
133
+ console.log(` ${c.bgRed(" ERROR ")} ${c.red(message)}`);
134
+ if (hint) {
135
+ console.log(` ${c.gray(hint)}`);
136
+ }
137
+ console.log();
138
+ }
139
+
140
+ /**
141
+ * Print a warning block.
142
+ */
143
+ export function warn(message, hint) {
144
+ console.log(` ${c.bgYellow(" WARN ")} ${c.yellow(message)}`);
145
+ if (hint) {
146
+ console.log(` ${c.gray(hint)}`);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Print a success block.
152
+ */
153
+ export function success(message) {
154
+ console.log();
155
+ console.log(` ${c.bgGreen(" OK ")} ${c.green(message)}`);
156
+ console.log();
157
+ }
158
+
159
+ /**
160
+ * Print a boxed list of items.
161
+ */
162
+ export function boxList(title, items) {
163
+ const w = Math.min(termWidth(), 68);
164
+ console.log();
165
+ console.log(c.gray(` ${BOX.tl}${BOX.h} ${c.bold(c.white(title))} ${c.gray(BOX.h.repeat(Math.max(0, w - stripAnsi(title).length - 6)))}${c.gray(BOX.tr)}`));
166
+ for (const item of items) {
167
+ const line = ` ${c.gray(BOX.v)} ${item}`;
168
+ console.log(line + " ".repeat(Math.max(0, w - stripAnsi(line).length + 2)) + c.gray(BOX.v));
169
+ }
170
+ console.log(c.gray(` ${BOX.bl}${BOX.h.repeat(w - 2)}${BOX.br}`));
171
+ }
172
+
173
+ // ── Spinner ────────────────────────────────────────────────────────────────
174
+
175
+ const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
176
+
177
+ export function createSpinner(text) {
178
+ let i = 0;
179
+ let interval = null;
180
+
181
+ return {
182
+ start() {
183
+ interval = setInterval(() => {
184
+ const frame = c.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length]);
185
+ stdout.write(`\r ${frame} ${text}`);
186
+ i++;
187
+ }, 80);
188
+ },
189
+ stop(finalText) {
190
+ if (interval) clearInterval(interval);
191
+ stdout.write(`\r ${c.green("\u2713")} ${finalText || text}\n`);
192
+ },
193
+ fail(finalText) {
194
+ if (interval) clearInterval(interval);
195
+ stdout.write(`\r ${c.red("\u2717")} ${finalText || text}\n`);
196
+ },
197
+ };
198
+ }
199
+
200
+ // ── Interactive prompts (zero-dep) ─────────────────────────────────────────
201
+
202
+ /**
203
+ * Ask a yes/no confirmation.
204
+ */
205
+ export async function confirm(question, defaultYes = true) {
206
+ const rl = createInterface({ input: stdin, output: stdout });
207
+ const hint = defaultYes ? "Y/n" : "y/N";
208
+ return new Promise((resolve) => {
209
+ rl.question(` ${c.cyan("?")} ${question} ${c.gray(`(${hint})`)} `, (answer) => {
210
+ rl.close();
211
+ const a = answer.trim().toLowerCase();
212
+ if (a === "") resolve(defaultYes);
213
+ else resolve(a === "y" || a === "yes");
214
+ });
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Ask for text input.
220
+ */
221
+ export async function input(question, defaultValue = "") {
222
+ const rl = createInterface({ input: stdin, output: stdout });
223
+ const hint = defaultValue ? c.gray(` (${defaultValue})`) : "";
224
+ return new Promise((resolve) => {
225
+ rl.question(` ${c.cyan("?")} ${question}${hint} `, (answer) => {
226
+ rl.close();
227
+ resolve(answer.trim() || defaultValue);
228
+ });
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Interactive single-select menu with arrow-key navigation.
234
+ * Returns the selected item object.
235
+ *
236
+ * @param {string} title - Prompt title
237
+ * @param {Array<{label: string, value: any, hint?: string}>} items
238
+ * @returns {Promise<{label: string, value: any}>}
239
+ */
240
+ export async function select(title, items) {
241
+ if (!items.length) throw new Error("select: no items provided");
242
+
243
+ return new Promise((resolve) => {
244
+ let cursor = 0;
245
+
246
+ const render = () => {
247
+ // Move cursor up to overwrite previous render
248
+ if (cursor !== -1) {
249
+ stdout.write(`\x1b[${items.length + 1}A`);
250
+ }
251
+
252
+ stdout.write(` ${c.cyan("?")} ${c.bold(title)}\n`);
253
+ for (let i = 0; i < items.length; i++) {
254
+ const item = items[i];
255
+ const selected = i === cursor;
256
+ const pointer = selected ? c.cyan("\u25B6") : " ";
257
+ const label = selected ? c.cyan(item.label) : c.white(item.label);
258
+ const hint = item.hint ? c.gray(` ${item.hint}`) : "";
259
+ stdout.write(` ${pointer} ${label}${hint}\n`);
260
+ }
261
+ };
262
+
263
+ // Initial render
264
+ stdout.write(` ${c.cyan("?")} ${c.bold(title)}\n`);
265
+ for (let i = 0; i < items.length; i++) {
266
+ const item = items[i];
267
+ const selected = i === cursor;
268
+ const pointer = selected ? c.cyan("\u25B6") : " ";
269
+ const label = selected ? c.cyan(item.label) : c.white(item.label);
270
+ const hint = item.hint ? c.gray(` ${item.hint}`) : "";
271
+ stdout.write(` ${pointer} ${label}${hint}\n`);
272
+ }
273
+
274
+ stdin.setRawMode(true);
275
+ stdin.resume();
276
+ stdin.setEncoding("utf8");
277
+
278
+ const onData = (key) => {
279
+ // Ctrl+C
280
+ if (key === "\x03") {
281
+ stdin.setRawMode(false);
282
+ stdin.removeListener("data", onData);
283
+ stdin.pause();
284
+ process.exit(130);
285
+ }
286
+ // Enter
287
+ if (key === "\r" || key === "\n") {
288
+ stdin.setRawMode(false);
289
+ stdin.removeListener("data", onData);
290
+ stdin.pause();
291
+ // Clear and reprint final selection
292
+ stdout.write(`\x1b[${items.length + 1}A`);
293
+ stdout.write(` ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[cursor].label)}\n`);
294
+ for (let i = 0; i < items.length; i++) {
295
+ stdout.write(`\x1b[2K\n`); // clear each line
296
+ }
297
+ stdout.write(`\x1b[${items.length}A`); // move back up
298
+ resolve(items[cursor]);
299
+ return;
300
+ }
301
+ // Arrow up / k
302
+ if (key === "\x1b[A" || key === "k") {
303
+ cursor = (cursor - 1 + items.length) % items.length;
304
+ render();
305
+ }
306
+ // Arrow down / j
307
+ if (key === "\x1b[B" || key === "j") {
308
+ cursor = (cursor + 1) % items.length;
309
+ render();
310
+ }
311
+ };
312
+
313
+ stdin.on("data", onData);
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Interactive multi-select menu with space to toggle and arrow keys to navigate.
319
+ * Returns array of selected item objects.
320
+ *
321
+ * @param {string} title - Prompt title
322
+ * @param {Array<{label: string, value: any, hint?: string, selected?: boolean}>} items
323
+ * @returns {Promise<Array<{label: string, value: any}>>}
324
+ */
325
+ export async function multiSelect(title, items) {
326
+ if (!items.length) throw new Error("multiSelect: no items provided");
327
+
328
+ return new Promise((resolve) => {
329
+ let cursor = 0;
330
+ const selected = new Set(
331
+ items.filter((it) => it.selected).map((_, i) => i)
332
+ );
333
+
334
+ const render = () => {
335
+ stdout.write(`\x1b[${items.length + 2}A`);
336
+ stdout.write(` ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle, enter to confirm)")}\n`);
337
+ for (let i = 0; i < items.length; i++) {
338
+ const item = items[i];
339
+ const isCursor = i === cursor;
340
+ const isSelected = selected.has(i);
341
+ const pointer = isCursor ? c.cyan("\u25B6") : " ";
342
+ const check = isSelected ? c.green("\u25C9") : c.gray("\u25CB");
343
+ const label = isCursor ? c.cyan(item.label) : c.white(item.label);
344
+ const hint = item.hint ? c.gray(` ${item.hint}`) : "";
345
+ stdout.write(`\x1b[2K ${pointer} ${check} ${label}${hint}\n`);
346
+ }
347
+ stdout.write("\x1b[2K\n");
348
+ };
349
+
350
+ // Initial render
351
+ stdout.write(` ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle, enter to confirm)")}\n`);
352
+ for (let i = 0; i < items.length; i++) {
353
+ const item = items[i];
354
+ const isCursor = i === cursor;
355
+ const isSelected = selected.has(i);
356
+ const pointer = isCursor ? c.cyan("\u25B6") : " ";
357
+ const check = isSelected ? c.green("\u25C9") : c.gray("\u25CB");
358
+ const label = isCursor ? c.cyan(item.label) : c.white(item.label);
359
+ const hint = item.hint ? c.gray(` ${item.hint}`) : "";
360
+ stdout.write(` ${pointer} ${check} ${label}${hint}\n`);
361
+ }
362
+ stdout.write("\n");
363
+
364
+ stdin.setRawMode(true);
365
+ stdin.resume();
366
+ stdin.setEncoding("utf8");
367
+
368
+ const onData = (key) => {
369
+ if (key === "\x03") {
370
+ stdin.setRawMode(false);
371
+ stdin.removeListener("data", onData);
372
+ stdin.pause();
373
+ process.exit(130);
374
+ }
375
+ if (key === "\r" || key === "\n") {
376
+ stdin.setRawMode(false);
377
+ stdin.removeListener("data", onData);
378
+ stdin.pause();
379
+ const result = [...selected].map((i) => items[i]);
380
+ // Clear and show final
381
+ stdout.write(`\x1b[${items.length + 2}A`);
382
+ stdout.write(`\x1b[2K ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(result.map((r) => r.label).join(", "))}\n`);
383
+ for (let i = 0; i < items.length + 1; i++) {
384
+ stdout.write(`\x1b[2K\n`);
385
+ }
386
+ stdout.write(`\x1b[${items.length + 1}A`);
387
+ resolve(result);
388
+ return;
389
+ }
390
+ if (key === " ") {
391
+ if (selected.has(cursor)) selected.delete(cursor);
392
+ else selected.add(cursor);
393
+ render();
394
+ }
395
+ if (key === "\x1b[A" || key === "k") {
396
+ cursor = (cursor - 1 + items.length) % items.length;
397
+ render();
398
+ }
399
+ if (key === "\x1b[B" || key === "j") {
400
+ cursor = (cursor + 1) % items.length;
401
+ render();
402
+ }
403
+ };
404
+
405
+ stdin.on("data", onData);
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Display a table with columns.
411
+ * @param {string[]} headers
412
+ * @param {string[][]} rows
413
+ */
414
+ export function table(headers, rows) {
415
+ const colWidths = headers.map((h, i) => {
416
+ const maxData = rows.reduce((max, row) => Math.max(max, stripAnsi(String(row[i] || "")).length), 0);
417
+ return Math.max(stripAnsi(h).length, maxData) + 2;
418
+ });
419
+
420
+ const headerLine = headers.map((h, i) => c.bold(h.padEnd(colWidths[i]))).join(c.gray(" \u2502 "));
421
+ console.log(` ${headerLine}`);
422
+ console.log(` ${colWidths.map((w) => c.gray(BOX.h.repeat(w))).join(c.gray("\u2500\u253c\u2500"))}`);
423
+
424
+ for (const row of rows) {
425
+ const line = row.map((cell, i) => String(cell || "").padEnd(colWidths[i])).join(c.gray(" \u2502 "));
426
+ console.log(` ${line}`);
427
+ }
428
+ }
429
+
430
+ // ── Utilities ──────────────────────────────────────────────────────────────
431
+
432
+ /**
433
+ * Strip ANSI escape codes from a string.
434
+ */
435
+ export function stripAnsi(str) {
436
+ // eslint-disable-next-line no-control-regex
437
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
438
+ }
439
+
440
+ /**
441
+ * Truncate a string to maxLen, adding ellipsis if needed.
442
+ */
443
+ export function truncate(str, maxLen = 40) {
444
+ if (str.length <= maxLen) return str;
445
+ return str.slice(0, maxLen - 1) + "\u2026";
446
+ }
@@ -21,7 +21,7 @@ You are the **Developer** — you implement code changes according to the plan p
21
21
 
22
22
  ## Context Loading Rule
23
23
  Before writing code, ALWAYS:
24
- 1. Read `.ai-starter-kit/context/runtime-context.md` for active preset and rules.
24
+ 1. Read `.opencode/context/runtime-context.md` for active preset and rules.
25
25
  2. Read the plan provided by the orchestrator or planner.
26
26
  3. Read existing files you will modify to understand current patterns.
27
27
  4. Check the preset file for framework-specific conventions (naming, structure, patterns).
@@ -20,7 +20,7 @@ You are the **Docs Agent** — you maintain project documentation so it stays ac
20
20
 
21
21
  ## Context Loading Rule
22
22
  Before writing docs, ALWAYS:
23
- 1. Read `.ai-starter-kit/context/runtime-context.md` for docs path and conventions.
23
+ 1. Read `.opencode/context/runtime-context.md` for docs path and conventions.
24
24
  2. Read the plan and implementation summary to understand what changed.
25
25
  3. Read existing documentation to avoid duplication.
26
26
  4. Check the preset file for framework-specific documentation conventions.
@@ -14,7 +14,7 @@ permission:
14
14
  You are the **Orchestrator** — the single entry point for all work in this project. You coordinate a team of specialist agents and ensure every task follows the defined workflow.
15
15
 
16
16
  ## Responsibilities
17
- 1. **Load context** — On every invocation, read `.ai-starter-kit/context/runtime-context.md` first. This contains the active preset, rules, and project documentation summary.
17
+ 1. **Load context** — On every invocation, read `.opencode/context/runtime-context.md` first. This contains the active preset, rules, and project documentation summary.
18
18
  2. **Read tasks** — Identify the current task from the PRD or task list being processed by Ralphy.
19
19
  3. **Route to specialists** — Delegate work to the correct sub-agent:
20
20
  - `@planner` for breaking objectives into steps
@@ -42,9 +42,9 @@ For each task:
42
42
 
43
43
  ## Context Loading Rule (CRITICAL)
44
44
  Before doing ANY work, you MUST:
45
- 1. Read `.ai-starter-kit/context/runtime-context.md`
46
- 2. Read `.ai-starter-kit/rules/universal.md`
47
- 3. Read the active preset file from `.ai-starter-kit/presets/<preset>.md`
45
+ 1. Read `.opencode/context/runtime-context.md`
46
+ 2. Read `.opencode/rules/universal.md`
47
+ 3. Read the active preset file from `.opencode/presets/<preset>.md`
48
48
  4. If `docsPath` is configured, scan that directory for relevant documentation
49
49
 
50
50
  Only after loading context should you begin task routing.
@@ -22,7 +22,7 @@ You are the **Planner** — an architect who turns high-level objectives into co
22
22
 
23
23
  ## Context Loading Rule
24
24
  Before planning, ALWAYS:
25
- 1. Read `.ai-starter-kit/context/runtime-context.md` for active preset and rules.
25
+ 1. Read `.opencode/context/runtime-context.md` for active preset and rules.
26
26
  2. Read relevant source files to understand current state.
27
27
  3. Check the preset file for framework-specific conventions.
28
28
 
@@ -23,7 +23,7 @@ You are the **QA Agent** — you verify that implementation meets requirements,
23
23
 
24
24
  ## Context Loading Rule
25
25
  Before testing, ALWAYS:
26
- 1. Read `.ai-starter-kit/context/runtime-context.md` for test commands and conventions.
26
+ 1. Read `.opencode/context/runtime-context.md` for test commands and conventions.
27
27
  2. Read the plan and acceptance criteria for the current task.
28
28
  3. Check the preset file for framework-specific test expectations.
29
29
 
@@ -22,7 +22,7 @@ You are the **Reviewer** — you perform code review on changes made by the Deve
22
22
 
23
23
  ## Context Loading Rule
24
24
  Before reviewing, ALWAYS:
25
- 1. Read `.ai-starter-kit/context/runtime-context.md` for project conventions.
25
+ 1. Read `.opencode/context/runtime-context.md` for project conventions.
26
26
  2. Read the plan to understand intent behind changes.
27
27
  3. Read the preset file for framework-specific review criteria.
28
28
 
@@ -1,19 +1,58 @@
1
1
  {
2
- "$schema": "https://github.com/ai-starter-kit/config-schema",
3
- "version": "0.1.0",
2
+ "$schema": "https://github.com/opencode-auto-agent/config-schema",
3
+ "version": "0.2.0",
4
4
  "preset": "java",
5
5
  "docsPath": "./Docs",
6
6
  "tasksPath": "./PRD.md",
7
7
  "outputPath": "./",
8
8
  "engine": "opencode",
9
+ "models": {
10
+ "orchestrator": "google/gemini-3-pro",
11
+ "planner": "google/gemini-3-pro",
12
+ "developer": "openai/gpt-5.1-codex",
13
+ "qa": "anthropic/claude-sonnet-4-5",
14
+ "reviewer": "anthropic/claude-sonnet-4-5",
15
+ "docs": "anthropic/claude-sonnet-4-5"
16
+ },
9
17
  "agents": {
10
- "orchestrator": { "enabled": true },
11
- "planner": { "enabled": true },
12
- "developer": { "enabled": true },
13
- "qa": { "enabled": true },
14
- "reviewer": { "enabled": true },
15
- "docs": { "enabled": true }
18
+ "orchestrator": {
19
+ "enabled": true,
20
+ "model": "google/gemini-3-pro",
21
+ "mode": "primary"
22
+ },
23
+ "planner": {
24
+ "enabled": true,
25
+ "model": "google/gemini-3-pro",
26
+ "mode": "subagent"
27
+ },
28
+ "developer": {
29
+ "enabled": true,
30
+ "model": "openai/gpt-5.1-codex",
31
+ "mode": "subagent"
32
+ },
33
+ "qa": {
34
+ "enabled": true,
35
+ "model": "anthropic/claude-sonnet-4-5",
36
+ "mode": "subagent"
37
+ },
38
+ "reviewer": {
39
+ "enabled": true,
40
+ "model": "anthropic/claude-sonnet-4-5",
41
+ "mode": "subagent"
42
+ },
43
+ "docs": {
44
+ "enabled": true,
45
+ "model": "anthropic/claude-sonnet-4-5",
46
+ "mode": "subagent"
47
+ }
16
48
  },
49
+ "rules": [
50
+ ".opencode/rules/universal.md"
51
+ ],
52
+ "instructions": [
53
+ "AGENTS.md",
54
+ ".opencode/rules/universal.md"
55
+ ],
17
56
  "orchestrator": {
18
57
  "workflowSteps": ["plan", "implement", "test", "verify"],
19
58
  "requirePlanApproval": false