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.
@@ -1,106 +1,201 @@
1
1
  /**
2
- * run command — start the orchestrator.
2
+ * run command — start the orchestrator via Ralphy + OpenCode.
3
3
  *
4
4
  * Flow:
5
- * 1. Load config.json
6
- * 2. Assemble runtime context (preset rules + base rules + docs)
7
- * 3. Write assembled context to .ai-starter-kit/context/
8
- * 4. Invoke Ralphy with --opencode engine against the PRD/task file
9
- * 5. Ralphy drives OpenCode, which loads the orchestrator agent and delegates
5
+ * 1. Validate prerequisites (require Ralphy for this command)
6
+ * 2. Load config.json (v0.2.0 schema with model mappings)
7
+ * 3. Assemble runtime context (preset rules + base rules + docs + model info)
8
+ * 4. Write assembled context to .opencode/context/
9
+ * 5. Show run summary with TUI
10
+ * 6. Invoke Ralphy with --opencode engine against the PRD/task file
11
+ * 7. Ralphy drives OpenCode, which loads the orchestrator agent and delegates
10
12
  */
11
13
 
12
14
  import { existsSync, readdirSync, readFileSync } from "node:fs";
13
15
  import { join } from "node:path";
14
- import { execSync, spawn } from "node:child_process";
16
+ import { spawn } from "node:child_process";
15
17
 
16
- import { SCAFFOLD_DIR, CONFIG_FILE, DIRS } from "../lib/constants.js";
18
+ import {
19
+ SCAFFOLD_DIR, CONFIG_FILE, DIRS, AGENTS, AGENT_ROLES,
20
+ } from "../lib/constants.js";
17
21
  import { readJsonSync, readTextSync, writeTextSync } from "../lib/fs-utils.js";
22
+ import { validateAndReport } from "../lib/prerequisites.js";
23
+ import {
24
+ banner, section, status, kv, error, warn, success, c, table, hr,
25
+ } from "../lib/ui.js";
18
26
 
27
+ /**
28
+ * @param {string} targetDir - Project root
29
+ * @param {Object} flags
30
+ * @param {string} [flags.engine] - Ralphy engine override
31
+ * @param {boolean} [flags.dryRun] - Assemble context but don't launch Ralphy
32
+ * @param {boolean} [flags.verbose] - Extra output
33
+ */
19
34
  export async function run(targetDir, flags = {}) {
35
+ banner("OpenCode Auto-Agent Runner", "Starting the AI agent orchestrator");
36
+
37
+ // ── 1. Prerequisites ──────────────────────────────────────────────────────
38
+ section("Prerequisites");
39
+ const prereqResult = validateAndReport({ requireRalphy: true, abortOnFail: true });
40
+ console.log();
41
+
42
+ // ── 2. Load config ────────────────────────────────────────────────────────
43
+ section("Configuration");
20
44
  const scaffoldDir = join(targetDir, SCAFFOLD_DIR);
21
45
  const configPath = join(scaffoldDir, CONFIG_FILE);
22
-
23
- // 1. Load config
24
46
  const config = readJsonSync(configPath);
47
+
25
48
  if (!config) {
26
- console.error(`[run] No config found at ${configPath}. Run 'opencode-auto-agent init' first.`);
49
+ error(
50
+ `No config found at ${SCAFFOLD_DIR}/${CONFIG_FILE}.`,
51
+ "Run 'npx opencode-auto-agent init' first."
52
+ );
27
53
  process.exit(1);
28
54
  }
29
55
 
30
- console.log(`[run] Preset: ${config.preset}`);
31
- console.log(`[run] Engine: ${flags.engine || config.engine || "opencode"}`);
56
+ const engine = flags.engine || config.engine || "opencode";
57
+ const preset = config.preset || "java";
32
58
 
33
- // 2. Assemble runtime context
34
- console.log("[run] Assembling runtime context...");
35
- const context = assembleContext(targetDir, config);
36
- const contextFile = join(scaffoldDir, DIRS.context, "runtime-context.md");
37
- writeTextSync(contextFile, context);
38
- console.log(`[run] Context written to ${DIRS.context}/runtime-context.md`);
59
+ kv("Preset", preset);
60
+ kv("Engine", engine);
61
+ kv("Schema version", config.version || "unknown");
39
62
 
40
- // 3. Determine task file
63
+ // Show model assignments
64
+ if (config.models) {
65
+ console.log();
66
+ table(
67
+ ["Agent", "Model", "Mode"],
68
+ AGENTS.filter((role) => config.agents?.[role]?.enabled !== false).map((role) => [
69
+ role,
70
+ c.cyan(config.models[role] || "(default)"),
71
+ c.gray(AGENT_ROLES[role]?.mode || "subagent"),
72
+ ])
73
+ );
74
+ }
75
+
76
+ // ── 3. Validate task file ─────────────────────────────────────────────────
77
+ section("Task File");
41
78
  const tasksPath = join(targetDir, config.tasksPath || "PRD.md");
79
+
42
80
  if (!existsSync(tasksPath)) {
43
- console.error(`[run] Task file not found: ${tasksPath}`);
44
- console.error(`[run] Create a PRD.md or update tasksPath in config.json.`);
81
+ error(
82
+ `Task file not found: ${config.tasksPath || "PRD.md"}`,
83
+ "Create a PRD.md or update 'tasksPath' in config.json."
84
+ );
45
85
  process.exit(1);
46
86
  }
47
87
 
48
- // 4. Invoke Ralphy
49
- const engine = flags.engine || config.engine || "opencode";
88
+ const taskContent = readTextSync(tasksPath);
89
+ const taskLines = taskContent ? taskContent.split("\n").length : 0;
90
+ status("pass", `Task file: ${config.tasksPath || "PRD.md"}`, `${taskLines} lines`);
91
+
92
+ // ── 4. Assemble runtime context ───────────────────────────────────────────
93
+ section("Context Assembly");
94
+ const context = assembleContext(targetDir, config);
95
+ const contextFile = join(scaffoldDir, DIRS.context, "runtime-context.md");
96
+ writeTextSync(contextFile, context);
97
+ status("pass", "Runtime context assembled", `${DIRS.context}/runtime-context.md`);
98
+
99
+ // ── 5. Dry run check ─────────────────────────────────────────────────────
100
+ if (flags.dryRun) {
101
+ success("Dry run complete. Context written but Ralphy not launched.");
102
+ console.log(` ${c.gray("Context file:")} ${contextFile}`);
103
+ console.log();
104
+ return;
105
+ }
106
+
107
+ // ── 6. Launch Ralphy ──────────────────────────────────────────────────────
108
+ section("Launching Orchestrator");
109
+ console.log();
110
+
50
111
  const ralphyArgs = [
51
112
  "--prd", tasksPath,
52
113
  `--${engine}`,
53
114
  ];
54
115
 
55
- console.log(`[run] Starting Ralphy: ralphy ${ralphyArgs.join(" ")}`);
56
- console.log("[run] ─────────────────────────────────────────────");
57
-
58
- const child = spawn("ralphy", ralphyArgs, {
59
- cwd: targetDir,
60
- stdio: "inherit",
61
- shell: true,
62
- env: {
63
- ...process.env,
64
- // Pass context location to OpenCode via env
65
- AISK_CONTEXT_DIR: join(scaffoldDir, DIRS.context),
66
- AISK_CONFIG_PATH: configPath,
67
- },
68
- });
116
+ status("arrow", `ralphy ${ralphyArgs.join(" ")}`);
117
+ hr();
118
+ console.log();
69
119
 
70
- child.on("close", (code) => {
71
- if (code === 0) {
72
- console.log("\n[run] Orchestrator completed successfully.");
73
- } else {
74
- console.error(`\n[run] Orchestrator exited with code ${code}.`);
75
- process.exit(code);
76
- }
77
- });
120
+ return new Promise((resolve, reject) => {
121
+ const child = spawn("ralphy", ralphyArgs, {
122
+ cwd: targetDir,
123
+ stdio: "inherit",
124
+ shell: true,
125
+ env: {
126
+ ...process.env,
127
+ // Pass context location to OpenCode via env
128
+ AISK_CONTEXT_DIR: join(scaffoldDir, DIRS.context),
129
+ AISK_CONFIG_PATH: configPath,
130
+ // Pass model info as env vars for downstream use
131
+ AISK_ORCHESTRATOR_MODEL: config.models?.orchestrator || "",
132
+ AISK_PRESET: preset,
133
+ },
134
+ });
78
135
 
79
- child.on("error", (err) => {
80
- if (err.code === "ENOENT") {
81
- console.error("[run] 'ralphy' CLI not found. Install it:");
82
- console.error(" npm install -g ralphy-cli");
83
- } else {
84
- console.error("[run] Failed to start Ralphy:", err.message);
85
- }
86
- process.exit(1);
136
+ child.on("close", (code) => {
137
+ console.log();
138
+ hr();
139
+ if (code === 0) {
140
+ success("Orchestrator completed successfully.");
141
+ } else {
142
+ error(
143
+ `Orchestrator exited with code ${code}.`,
144
+ "Check the output above for details."
145
+ );
146
+ process.exit(code);
147
+ }
148
+ resolve();
149
+ });
150
+
151
+ child.on("error", (err) => {
152
+ console.log();
153
+ if (err.code === "ENOENT") {
154
+ error(
155
+ "'ralphy' CLI not found.",
156
+ "Install: npm install -g ralphy-cli"
157
+ );
158
+ } else {
159
+ error(
160
+ `Failed to start Ralphy: ${err.message}`,
161
+ "Ensure ralphy is installed and accessible."
162
+ );
163
+ }
164
+ process.exit(1);
165
+ });
87
166
  });
88
167
  }
89
168
 
90
- // ── context assembly ───────────────────────────────────────────────────────
169
+ // ── Context assembly ───────────────────────────────────────────────────────
91
170
 
92
171
  function assembleContext(targetDir, config) {
93
172
  const scaffoldDir = join(targetDir, SCAFFOLD_DIR);
94
173
  const sections = [];
95
174
 
96
175
  sections.push("# Runtime Context (auto-generated)\n");
176
+ sections.push(`> Generated by opencode-auto-agent at ${new Date().toISOString()}`);
97
177
  sections.push(`Preset: **${config.preset}**`);
98
- sections.push(`Workflow: ${(config.orchestrator?.workflowSteps || []).join("")}\n`);
178
+ sections.push(`Engine: **${config.engine || "opencode"}**`);
179
+ sections.push(`Workflow: ${(config.orchestrator?.workflowSteps || []).join(" \u2192 ")}\n`);
180
+
181
+ // Model assignments
182
+ if (config.models) {
183
+ sections.push("## Agent Model Assignments\n");
184
+ sections.push("| Agent | Model | Mode |");
185
+ sections.push("| --- | --- | --- |");
186
+ for (const role of AGENTS) {
187
+ if (config.agents?.[role]?.enabled === false) continue;
188
+ const model = config.models[role] || "(default)";
189
+ const mode = AGENT_ROLES[role]?.mode || "subagent";
190
+ sections.push(`| ${role} | ${model} | ${mode} |`);
191
+ }
192
+ sections.push("");
193
+ }
99
194
 
100
195
  // Base rules
101
196
  const rulesDir = join(scaffoldDir, DIRS.rules);
102
197
  if (existsSync(rulesDir)) {
103
- for (const file of readdirSync(rulesDir).filter((f) => f.endsWith(".md"))) {
198
+ for (const file of safeReaddir(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
104
199
  const content = readTextSync(join(rulesDir, file));
105
200
  if (content) {
106
201
  sections.push(`## Rules: ${file}\n${content}`);
@@ -114,7 +209,7 @@ function assembleContext(targetDir, config) {
114
209
  if (presetContent) {
115
210
  sections.push(`## Preset: ${config.preset}\n${presetContent}`);
116
211
  } else {
117
- sections.push(`## Preset: ${config.preset}\n(No preset file found at ${presetFile})`);
212
+ sections.push(`## Preset: ${config.preset}\n(No preset file found)`);
118
213
  }
119
214
 
120
215
  // Repo docs (if configured and present)
@@ -122,7 +217,7 @@ function assembleContext(targetDir, config) {
122
217
  if (existsSync(docsPath)) {
123
218
  sections.push(`## Project Documentation\nDocs location: ${docsPath}`);
124
219
  try {
125
- const docFiles = readdirSync(docsPath).filter((f) => f.endsWith(".md"));
220
+ const docFiles = safeReaddir(docsPath).filter((f) => f.endsWith(".md"));
126
221
  for (const file of docFiles.slice(0, 10)) {
127
222
  const content = readTextSync(join(docsPath, file));
128
223
  if (content) {
@@ -134,14 +229,40 @@ function assembleContext(targetDir, config) {
134
229
  }
135
230
  }
136
231
 
232
+ // Instructions (from config)
233
+ if (Array.isArray(config.instructions) && config.instructions.length > 0) {
234
+ sections.push("## Instructions\n");
235
+ for (const instrPath of config.instructions) {
236
+ const resolved = join(targetDir, instrPath);
237
+ if (existsSync(resolved)) {
238
+ const content = readTextSync(resolved);
239
+ if (content) {
240
+ sections.push(`### ${instrPath}\n${content.slice(0, 3000)}`);
241
+ }
242
+ } else {
243
+ sections.push(`- ${instrPath} (not found)`);
244
+ }
245
+ }
246
+ }
247
+
137
248
  // Agent manifest
138
249
  sections.push("## Agent Team");
139
250
  const agentsDir = join(scaffoldDir, DIRS.agents);
140
251
  if (existsSync(agentsDir)) {
141
- for (const file of readdirSync(agentsDir).filter((f) => f.endsWith(".md")).sort()) {
142
- sections.push(`- ${file.replace(".md", "")}`);
252
+ for (const file of safeReaddir(agentsDir).filter((f) => f.endsWith(".md")).sort()) {
253
+ const role = file.replace(".md", "");
254
+ const model = config.models?.[role] || "default";
255
+ sections.push(`- **${role}** \u2014 ${AGENT_ROLES[role]?.description || role} (model: ${model})`);
143
256
  }
144
257
  }
145
258
 
146
259
  return sections.join("\n\n");
147
260
  }
261
+
262
+ function safeReaddir(dir) {
263
+ try {
264
+ return readdirSync(dir);
265
+ } catch {
266
+ return [];
267
+ }
268
+ }
@@ -1,40 +1,340 @@
1
1
  /**
2
- * setup command — apply or switch a preset.
2
+ * setup command — switch presets and optionally reconfigure model assignments.
3
3
  *
4
- * This updates config.json and regenerates the runtime context references.
4
+ * Updates both .opencode/config.json and the project-root opencode.json
5
+ * to keep them in sync. Can also update agent .md frontmatter with new models.
6
+ *
7
+ * Flow:
8
+ * 1. Verify scaffold exists
9
+ * 2. Load current config
10
+ * 3. Validate or interactively select preset
11
+ * 4. Optionally reconfigure model assignments
12
+ * 5. Write updated config.json
13
+ * 6. Write updated opencode.json
14
+ * 7. Update agent .md frontmatter (if models changed)
5
15
  */
6
16
 
7
- import { existsSync } from "node:fs";
8
- import { join } from "node:path";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join, resolve } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ import {
22
+ SCAFFOLD_DIR, CONFIG_FILE, PRESETS, AGENTS, AGENT_ROLES,
23
+ DIRS, DEFAULT_MODEL_MAP, WORKFLOW_STEPS, SCHEMA_VERSION,
24
+ } from "../lib/constants.js";
25
+ import { readJsonSync, writeJsonSync, writeTextSync } from "../lib/fs-utils.js";
26
+ import { discoverModelsInteractive, findModel, groupByProvider } from "../lib/models.js";
27
+ import {
28
+ banner, section, status, kv, success, error, warn, c,
29
+ select, confirm, table, boxList,
30
+ } from "../lib/ui.js";
9
31
 
10
- import { SCAFFOLD_DIR, CONFIG_FILE, PRESETS } from "../lib/constants.js";
11
- import { readJsonSync, writeJsonSync, readTextSync } from "../lib/fs-utils.js";
32
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
33
+ const TEMPLATES_ROOT = resolve(__dirname, "..", "..", "templates");
34
+
35
+ /**
36
+ * @param {string} targetDir - Project root
37
+ * @param {string|null} preset - Preset name (null triggers interactive selection)
38
+ * @param {Object} opts
39
+ * @param {boolean} [opts.models] - Also reconfigure model assignments
40
+ * @param {boolean} [opts.nonInteractive] - Skip prompts, use defaults
41
+ */
42
+ export async function setup(targetDir, preset, { models: reconfigModels, nonInteractive } = {}) {
43
+ banner("OpenCode Auto-Agent Setup", "Switch preset or reconfigure models");
12
44
 
13
- export async function setup(targetDir, preset) {
14
45
  const scaffoldDir = join(targetDir, SCAFFOLD_DIR);
15
46
  const configPath = join(scaffoldDir, CONFIG_FILE);
16
47
 
48
+ // ── 1. Verify scaffold ────────────────────────────────────────────────────
17
49
  if (!existsSync(configPath)) {
18
- console.error(`[setup] No ${SCAFFOLD_DIR}/ found. Run 'opencode-auto-agent init' first.`);
50
+ error(
51
+ `No ${SCAFFOLD_DIR}/ found.`,
52
+ "Run 'npx opencode-auto-agent init' first."
53
+ );
19
54
  process.exit(1);
20
55
  }
21
56
 
22
- // Validate preset
23
- const presetFile = join(scaffoldDir, "presets", `${preset}.md`);
24
- if (!existsSync(presetFile)) {
25
- console.error(`[setup] Unknown preset: "${preset}"`);
26
- console.error(`[setup] Available: ${PRESETS.join(", ")}`);
27
- console.error(`[setup] Or create a custom preset at: presets/${preset}.md`);
57
+ // ── 2. Load current config ────────────────────────────────────────────────
58
+ const config = readJsonSync(configPath);
59
+ if (!config) {
60
+ error("Failed to read config.json.", "File may be corrupted. Re-run init with --force.");
28
61
  process.exit(1);
29
62
  }
30
63
 
31
- // Update config
32
- const config = readJsonSync(configPath);
33
- const oldPreset = config.preset;
34
- config.preset = preset;
64
+ section("Current Configuration");
65
+ kv("Preset", config.preset || "(none)");
66
+ kv("Schema version", config.version || "(unknown)");
67
+ if (config.models) {
68
+ for (const [role, model] of Object.entries(config.models)) {
69
+ kv(` ${role}`, model);
70
+ }
71
+ }
72
+
73
+ // ── 3. Select preset ──────────────────────────────────────────────────────
74
+ section("Preset Selection");
75
+ let chosenPreset = preset;
76
+
77
+ if (!chosenPreset && !nonInteractive) {
78
+ const presetItems = PRESETS.map((p) => ({
79
+ label: p,
80
+ value: p,
81
+ hint: `${presetDescription(p)}${p === config.preset ? " (current)" : ""}`,
82
+ }));
83
+ const result = await select("Choose a project preset", presetItems);
84
+ chosenPreset = result.value;
85
+ }
86
+
87
+ if (chosenPreset && !PRESETS.includes(chosenPreset)) {
88
+ // Check if it exists as a custom preset file
89
+ const customPreset = join(scaffoldDir, DIRS.presets, `${chosenPreset}.md`);
90
+ if (!existsSync(customPreset)) {
91
+ error(
92
+ `Unknown preset: "${chosenPreset}"`,
93
+ `Available: ${PRESETS.join(", ")} (or create a custom preset at ${DIRS.presets}/${chosenPreset}.md)`
94
+ );
95
+ process.exit(1);
96
+ }
97
+ }
98
+
99
+ chosenPreset = chosenPreset || config.preset || "java";
100
+ const presetChanged = chosenPreset !== config.preset;
101
+ status(presetChanged ? "arrow" : "pass",
102
+ presetChanged
103
+ ? `Preset: ${c.yellow(config.preset)} ${c.gray("\u2192")} ${c.green(chosenPreset)}`
104
+ : `Preset: ${c.bold(chosenPreset)} ${c.gray("(unchanged)")}`
105
+ );
106
+
107
+ // ── 4. Model reconfiguration ──────────────────────────────────────────────
108
+ let modelMapping = config.models || { ...DEFAULT_MODEL_MAP };
109
+ let modelsChanged = false;
110
+
111
+ // Ask about model reconfiguration if preset changed or explicitly requested
112
+ const shouldReconfigModels = reconfigModels ||
113
+ (!nonInteractive && presetChanged &&
114
+ await confirm("Reconfigure model assignments for the new preset?", false));
115
+
116
+ if (shouldReconfigModels && !nonInteractive) {
117
+ section("Model Discovery");
118
+ const models = discoverModelsInteractive();
119
+
120
+ if (models.length > 0) {
121
+ const groups = groupByProvider(models);
122
+ const providerList = [...groups.entries()].map(
123
+ ([p, ms]) => `${c.bold(p)} ${c.gray(`(${ms.length} models)`)}`
124
+ );
125
+ boxList("Available Providers", providerList);
126
+
127
+ section("Agent Model Assignment");
128
+ console.log(` ${c.gray("Select a model for each agent role. Current assignment shown.")}`);
129
+ console.log();
130
+
131
+ for (const role of AGENTS) {
132
+ const currentModel = modelMapping[role] || DEFAULT_MODEL_MAP[role];
133
+ const currentFound = findModel(models, currentModel);
134
+
135
+ // Build selection: current first, default second (if different), then rest
136
+ const items = [];
137
+ const seen = new Set();
138
+
139
+ if (currentFound) {
140
+ items.push({ label: currentFound.id, value: currentFound.id, hint: "(current)" });
141
+ seen.add(currentFound.id);
142
+ }
143
+
144
+ const defaultId = DEFAULT_MODEL_MAP[role];
145
+ const defaultFound = findModel(models, defaultId);
146
+ if (defaultFound && !seen.has(defaultFound.id)) {
147
+ items.push({ label: defaultFound.id, value: defaultFound.id, hint: "(recommended)" });
148
+ seen.add(defaultFound.id);
149
+ }
150
+
151
+ for (const m of models) {
152
+ if (!seen.has(m.id)) {
153
+ items.push({ label: m.id, value: m.id });
154
+ }
155
+ }
156
+
157
+ const result = await select(
158
+ `Model for ${c.bold(role)} ${c.gray(`\u2014 ${AGENT_ROLES[role].description}`)}`,
159
+ items
160
+ );
161
+ if (result.value !== currentModel) modelsChanged = true;
162
+ modelMapping[role] = result.value;
163
+ }
164
+ } else {
165
+ warn("No models found. Keeping current model assignments.", "Configure opencode providers first.");
166
+ }
167
+ } else if (shouldReconfigModels && nonInteractive) {
168
+ status("info", "Model reconfiguration skipped in non-interactive mode");
169
+ }
170
+
171
+ // ── 5. Show summary ──────────────────────────────────────────────────────
172
+ if (presetChanged || modelsChanged) {
173
+ section("Changes Summary");
174
+ if (presetChanged) {
175
+ kv("Preset", `${config.preset} \u2192 ${chosenPreset}`);
176
+ }
177
+ if (modelsChanged) {
178
+ console.log();
179
+ table(
180
+ ["Agent Role", "Model", "Mode"],
181
+ AGENTS.map((role) => [
182
+ c.bold(role),
183
+ c.cyan(modelMapping[role]),
184
+ c.gray(AGENT_ROLES[role].mode),
185
+ ])
186
+ );
187
+ }
188
+
189
+ if (!nonInteractive) {
190
+ console.log();
191
+ const proceed = await confirm("Apply these changes?", true);
192
+ if (!proceed) {
193
+ console.log(c.gray("\n Aborted.\n"));
194
+ process.exit(0);
195
+ }
196
+ }
197
+ } else {
198
+ console.log();
199
+ status("info", "No changes to apply.");
200
+ return;
201
+ }
202
+
203
+ // ── 6. Write updated config.json ──────────────────────────────────────────
204
+ section("Applying Changes");
205
+
206
+ config.preset = chosenPreset;
207
+ config.models = modelMapping;
208
+ config.version = SCHEMA_VERSION;
209
+
210
+ // Update agents section
211
+ if (!config.agents) config.agents = {};
212
+ for (const role of AGENTS) {
213
+ if (!config.agents[role]) config.agents[role] = {};
214
+ config.agents[role].enabled = config.agents[role]?.enabled ?? true;
215
+ config.agents[role].model = modelMapping[role];
216
+ config.agents[role].mode = AGENT_ROLES[role].mode;
217
+ }
218
+
35
219
  writeJsonSync(configPath, config);
220
+ status("pass", "Updated config.json");
221
+
222
+ // ── 7. Write updated opencode.json ────────────────────────────────────────
223
+ const opencodeConfigPath = join(targetDir, "opencode.json");
224
+ const opencodeConfig = readJsonSync(opencodeConfigPath) || {};
225
+
226
+ // Update root model
227
+ opencodeConfig.$schema = opencodeConfig.$schema || "https://opencode.ai/config.json";
228
+ opencodeConfig.model = modelMapping.orchestrator;
229
+ opencodeConfig.instructions = opencodeConfig.instructions || [
230
+ "AGENTS.md",
231
+ `.opencode/rules/universal.md`,
232
+ ];
233
+
234
+ // Update agent definitions
235
+ if (!opencodeConfig.agent) opencodeConfig.agent = {};
236
+ for (const role of AGENTS) {
237
+ const def = AGENT_ROLES[role];
238
+ if (!opencodeConfig.agent[role]) {
239
+ opencodeConfig.agent[role] = {
240
+ description: def.description,
241
+ mode: def.mode,
242
+ prompt: buildAgentPrompt(role, chosenPreset),
243
+ tools: def.tools,
244
+ };
245
+ if (Object.keys(def.permission).length > 0) {
246
+ opencodeConfig.agent[role].permission = def.permission;
247
+ }
248
+ }
249
+ opencodeConfig.agent[role].model = modelMapping[role];
250
+ opencodeConfig.agent[role].temperature = def.temperature;
251
+ }
252
+
253
+ writeJsonSync(opencodeConfigPath, opencodeConfig);
254
+ status("pass", "Updated opencode.json");
255
+
256
+ // ── 8. Update agent markdown frontmatter ──────────────────────────────────
257
+ if (modelsChanged) {
258
+ const agentsDir = join(scaffoldDir, DIRS.agents);
259
+ for (const role of AGENTS) {
260
+ const agentFile = join(agentsDir, `${role}.md`);
261
+ if (existsSync(agentFile)) {
262
+ try {
263
+ const content = readFileSync(agentFile, "utf8");
264
+ const patched = patchAgentModelFrontmatter(content, modelMapping[role]);
265
+ if (patched !== content) {
266
+ writeTextSync(agentFile, patched);
267
+ }
268
+ } catch {
269
+ // Non-fatal: markdown patching failure
270
+ }
271
+ }
272
+ }
273
+ status("pass", "Updated agent markdown frontmatter");
274
+ }
275
+
276
+ // ── Done ──────────────────────────────────────────────────────────────────
277
+ success("Setup complete!");
278
+
279
+ console.log(` ${c.bold("Next steps:")}`);
280
+ console.log(` ${c.gray("1.")} Run ${c.cyan("npx opencode-auto-agent doctor")} to validate`);
281
+ console.log(` ${c.gray("2.")} Run ${c.cyan("npx opencode-auto-agent run")} to start the orchestrator`);
282
+ console.log();
283
+ }
284
+
285
+ // ── Helpers ────────────────────────────────────────────────────────────────
286
+
287
+ function presetDescription(name) {
288
+ const desc = {
289
+ java: "General Java project (Maven/Gradle)",
290
+ springboot: "Spring Boot 3.x web application",
291
+ nextjs: "Next.js 14+ with App Router",
292
+ };
293
+ return desc[name] || "Custom preset";
294
+ }
295
+
296
+ function buildAgentPrompt(role, preset) {
297
+ const prompts = {
298
+ orchestrator:
299
+ `You are the orchestrator. Read .opencode/context/ for active context. Follow the workflow: plan -> implement -> test -> verify. Preset: ${preset}.`,
300
+ planner:
301
+ "You are the planner agent. Produce step-by-step implementation plans. Do not write code. Output plans as numbered markdown lists.",
302
+ developer:
303
+ "You are the developer agent. Implement code changes per the plan. Follow project conventions. Write clean, tested code.",
304
+ qa:
305
+ "You are the QA agent. Run tests, check for regressions, verify acceptance criteria. Report pass/fail status clearly.",
306
+ reviewer:
307
+ "You are the reviewer agent. Review code for correctness, security issues, performance problems, and style violations. Do not make changes.",
308
+ docs:
309
+ "You are the docs agent. Update documentation to reflect code changes. Keep README, API docs, and inline comments accurate.",
310
+ };
311
+ return prompts[role] || `You are the ${role} agent.`;
312
+ }
313
+
314
+ /**
315
+ * Patch or add the `model:` field in markdown YAML frontmatter.
316
+ */
317
+ function patchAgentModelFrontmatter(content, modelId) {
318
+ if (!content.startsWith("---")) return content;
319
+
320
+ const endIdx = content.indexOf("---", 3);
321
+ if (endIdx === -1) return content;
322
+
323
+ let frontmatter = content.slice(3, endIdx);
324
+ const body = content.slice(endIdx);
325
+
326
+ if (/^model:/m.test(frontmatter)) {
327
+ frontmatter = frontmatter.replace(/^model:.*$/m, `model: ${modelId}`);
328
+ } else {
329
+ const lines = frontmatter.split("\n");
330
+ const descIdx = lines.findIndex((l) => l.startsWith("description:"));
331
+ if (descIdx >= 0) {
332
+ lines.splice(descIdx + 1, 0, `model: ${modelId}`);
333
+ } else {
334
+ lines.push(`model: ${modelId}`);
335
+ }
336
+ frontmatter = lines.join("\n");
337
+ }
36
338
 
37
- console.log(`[setup] Preset changed: ${oldPreset}${preset}`);
38
- console.log(`[setup] Preset file: presets/${preset}.md`);
39
- console.log(`[setup] Run 'opencode-auto-agent run' to use the new preset.`);
339
+ return `---${frontmatter}${body}`;
40
340
  }