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/README.md +24 -6
- package/bin/cli.js +275 -51
- package/package.json +1 -1
- package/src/commands/doctor.js +337 -54
- package/src/commands/init.js +337 -114
- package/src/commands/run.js +182 -61
- package/src/commands/setup.js +321 -21
- package/src/lib/constants.js +91 -15
- package/src/lib/models.js +172 -0
- package/src/lib/prerequisites.js +151 -0
- package/src/lib/ui.js +446 -0
- package/templates/agents/developer.md +1 -1
- package/templates/agents/docs.md +1 -1
- package/templates/agents/orchestrator.md +4 -4
- package/templates/agents/planner.md +1 -1
- package/templates/agents/qa.md +1 -1
- package/templates/agents/reviewer.md +1 -1
- package/templates/sample-config.json +47 -8
package/src/commands/run.js
CHANGED
|
@@ -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.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
9
|
-
* 5.
|
|
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 {
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
15
17
|
|
|
16
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
56
|
+
const engine = flags.engine || config.engine || "opencode";
|
|
57
|
+
const preset = config.preset || "java";
|
|
32
58
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
const
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
// ──
|
|
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(`
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
142
|
-
|
|
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
|
+
}
|
package/src/commands/setup.js
CHANGED
|
@@ -1,40 +1,340 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* setup command —
|
|
2
|
+
* setup command — switch presets and optionally reconfigure model assignments.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
config.
|
|
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
|
-
|
|
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
|
}
|