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/doctor.js
CHANGED
|
@@ -1,91 +1,374 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* doctor command — validate configuration
|
|
2
|
+
* doctor command — validate configuration, tools, model assignments, and
|
|
3
|
+
* full alignment with the OpenCode agent/tool/rule ecosystem.
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* 1. Prerequisites (node, opencode, ralphy, git)
|
|
7
|
+
* 2. Scaffold directory structure (.opencode/)
|
|
8
|
+
* 3. config.json schema & version
|
|
9
|
+
* 4. Model assignments exist and are still available via `opencode models`
|
|
10
|
+
* 5. opencode.json alignment (agents, model, instructions)
|
|
11
|
+
* 6. Agent markdown files (frontmatter, model field)
|
|
12
|
+
* 7. Rules and presets
|
|
13
|
+
* 8. Ralphy config
|
|
14
|
+
* 9. Tasks/PRD file
|
|
3
15
|
*/
|
|
4
16
|
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
17
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
6
18
|
import { join } from "node:path";
|
|
7
|
-
import { execSync } from "node:child_process";
|
|
8
19
|
|
|
9
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
SCAFFOLD_DIR, CONFIG_FILE, DIRS, AGENTS, AGENT_ROLES,
|
|
22
|
+
PRESETS, SCHEMA_VERSION,
|
|
23
|
+
} from "../lib/constants.js";
|
|
10
24
|
import { readJsonSync } from "../lib/fs-utils.js";
|
|
25
|
+
import { validatePrerequisites } from "../lib/prerequisites.js";
|
|
26
|
+
import { discoverModels, findModel } from "../lib/models.js";
|
|
27
|
+
import {
|
|
28
|
+
banner, section, status, kv, error, warn, success, c, table, hr,
|
|
29
|
+
createSpinner,
|
|
30
|
+
} from "../lib/ui.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} targetDir - Project root
|
|
34
|
+
* @param {Object} opts
|
|
35
|
+
* @param {boolean} [opts.verbose] - Show extra detail
|
|
36
|
+
*/
|
|
37
|
+
export async function doctor(targetDir, { verbose = false } = {}) {
|
|
38
|
+
banner("OpenCode Auto-Agent Doctor", "Validating project configuration");
|
|
11
39
|
|
|
12
|
-
export async function doctor(targetDir) {
|
|
13
|
-
const scaffoldDir = join(targetDir, SCAFFOLD_DIR);
|
|
14
40
|
let errors = 0;
|
|
15
41
|
let warnings = 0;
|
|
16
42
|
|
|
17
|
-
|
|
43
|
+
const pass = (label, detail) => { status("pass", label, detail); };
|
|
44
|
+
const fail = (label, detail) => { status("fail", label, detail); errors++; };
|
|
45
|
+
const softWarn = (label, detail) => { status("warn", label, detail); warnings++; };
|
|
46
|
+
|
|
47
|
+
// ── 1. Prerequisites ──────────────────────────────────────────────────────
|
|
48
|
+
section("Prerequisites");
|
|
49
|
+
const prereqs = validatePrerequisites({ requireRalphy: false });
|
|
50
|
+
|
|
51
|
+
for (const ch of prereqs.checks) {
|
|
52
|
+
if (ch.ok) {
|
|
53
|
+
pass(ch.name, `v${ch.version}`);
|
|
54
|
+
} else if (ch.name.startsWith("Node") || ch.name.startsWith("OpenCode")) {
|
|
55
|
+
fail(ch.name, "not found");
|
|
56
|
+
console.log(` ${c.gray(ch.hint)}`);
|
|
57
|
+
} else {
|
|
58
|
+
softWarn(ch.name, "not found (optional)");
|
|
59
|
+
console.log(` ${c.gray(ch.hint)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 2. Scaffold directory ─────────────────────────────────────────────────
|
|
64
|
+
section("Project Structure");
|
|
65
|
+
const scaffoldDir = join(targetDir, SCAFFOLD_DIR);
|
|
66
|
+
|
|
67
|
+
if (!existsSync(scaffoldDir)) {
|
|
68
|
+
fail(`${SCAFFOLD_DIR}/ directory`, "not found");
|
|
69
|
+
error(
|
|
70
|
+
`No ${SCAFFOLD_DIR}/ directory found.`,
|
|
71
|
+
"Run 'npx opencode-auto-agent init' to scaffold the project."
|
|
72
|
+
);
|
|
73
|
+
printSummary(errors, warnings);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
pass(`${SCAFFOLD_DIR}/ directory`, "exists");
|
|
18
77
|
|
|
19
|
-
//
|
|
20
|
-
|
|
78
|
+
// Check subdirectories
|
|
79
|
+
for (const [key, sub] of Object.entries(DIRS)) {
|
|
80
|
+
const subPath = join(scaffoldDir, sub);
|
|
81
|
+
if (existsSync(subPath)) {
|
|
82
|
+
const count = safeReaddir(subPath).length;
|
|
83
|
+
pass(`${SCAFFOLD_DIR}/${sub}/`, `${count} file(s)`);
|
|
84
|
+
} else {
|
|
85
|
+
fail(`${SCAFFOLD_DIR}/${sub}/`, "missing");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
21
88
|
|
|
22
|
-
//
|
|
89
|
+
// ── 3. config.json ────────────────────────────────────────────────────────
|
|
90
|
+
section("Configuration");
|
|
23
91
|
const configPath = join(scaffoldDir, CONFIG_FILE);
|
|
24
92
|
const config = readJsonSync(configPath);
|
|
25
|
-
check("Config file", !!config, `${CONFIG_FILE} missing or invalid JSON.`);
|
|
26
93
|
|
|
27
|
-
if (config) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
94
|
+
if (!config) {
|
|
95
|
+
fail(CONFIG_FILE, "missing or invalid JSON");
|
|
96
|
+
} else {
|
|
97
|
+
pass(CONFIG_FILE, "valid JSON");
|
|
31
98
|
|
|
32
|
-
//
|
|
99
|
+
// Schema version
|
|
100
|
+
if (config.version === SCHEMA_VERSION) {
|
|
101
|
+
pass("Config version", `v${config.version}`);
|
|
102
|
+
} else if (config.version) {
|
|
103
|
+
softWarn("Config version", `v${config.version} (current: ${SCHEMA_VERSION})`);
|
|
104
|
+
} else {
|
|
105
|
+
softWarn("Config version", "missing (expected " + SCHEMA_VERSION + ")");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Preset
|
|
109
|
+
if (config.preset) {
|
|
110
|
+
const presetFile = join(scaffoldDir, DIRS.presets, `${config.preset}.md`);
|
|
111
|
+
if (existsSync(presetFile)) {
|
|
112
|
+
pass(`Preset "${config.preset}"`, "file found");
|
|
113
|
+
} else {
|
|
114
|
+
fail(`Preset "${config.preset}"`, `presets/${config.preset}.md not found`);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
softWarn("Preset", "not set in config");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Models section
|
|
121
|
+
if (config.models && typeof config.models === "object") {
|
|
122
|
+
pass("Models mapping", `${Object.keys(config.models).length} agent(s) configured`);
|
|
123
|
+
} else {
|
|
124
|
+
softWarn("Models mapping", "missing from config (legacy v0.1.0 format?)");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Agents section
|
|
128
|
+
if (config.agents && typeof config.agents === "object") {
|
|
129
|
+
const enabledCount = Object.values(config.agents).filter((a) => a.enabled).length;
|
|
130
|
+
pass("Agents config", `${enabledCount}/${Object.keys(config.agents).length} enabled`);
|
|
131
|
+
} else {
|
|
132
|
+
softWarn("Agents config", "missing from config");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Paths
|
|
33
136
|
const tasksPath = join(targetDir, config.tasksPath || "PRD.md");
|
|
34
|
-
|
|
35
|
-
|
|
137
|
+
if (existsSync(tasksPath)) {
|
|
138
|
+
pass("Tasks file", config.tasksPath || "PRD.md");
|
|
139
|
+
} else {
|
|
140
|
+
softWarn("Tasks file", `${config.tasksPath || "PRD.md"} not found`);
|
|
141
|
+
}
|
|
36
142
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
143
|
+
const docsPath = join(targetDir, config.docsPath || "Docs");
|
|
144
|
+
if (existsSync(docsPath)) {
|
|
145
|
+
pass("Docs directory", config.docsPath || "Docs");
|
|
146
|
+
} else {
|
|
147
|
+
softWarn("Docs directory", `${config.docsPath || "Docs"} not found (optional)`);
|
|
148
|
+
}
|
|
41
149
|
}
|
|
42
150
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
check("Universal rules", existsSync(rulesDir), "Missing rules/universal.md");
|
|
46
|
-
|
|
47
|
-
// 7. External tools
|
|
48
|
-
checkTool("ralphy", "npm install -g ralphy-cli");
|
|
49
|
-
checkTool("opencode", "npm install -g opencode-ai");
|
|
151
|
+
// ── 4. Model validation against live discovery ────────────────────────────
|
|
152
|
+
section("Model Validation");
|
|
50
153
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
154
|
+
if (config && config.models) {
|
|
155
|
+
const spinner = createSpinner("Checking model availability via opencode...");
|
|
156
|
+
spinner.start();
|
|
157
|
+
const availableModels = discoverModels();
|
|
158
|
+
if (availableModels.length === 0) {
|
|
159
|
+
spinner.fail("Could not discover models (opencode may not be configured)");
|
|
160
|
+
softWarn("Model validation", "skipped — no models discovered");
|
|
161
|
+
} else {
|
|
162
|
+
spinner.stop(`Discovered ${availableModels.length} models`);
|
|
54
163
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
164
|
+
for (const [role, modelId] of Object.entries(config.models)) {
|
|
165
|
+
const found = findModel(availableModels, modelId);
|
|
166
|
+
if (found) {
|
|
167
|
+
pass(`${padRole(role)} ${c.cyan(modelId)}`, "available");
|
|
168
|
+
} else {
|
|
169
|
+
fail(`${padRole(role)} ${c.cyan(modelId)}`, "NOT available from opencode");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
58
173
|
} else {
|
|
59
|
-
|
|
174
|
+
softWarn("Model validation", "skipped — no models mapping in config");
|
|
60
175
|
}
|
|
61
176
|
|
|
62
|
-
// ──
|
|
177
|
+
// ── 5. opencode.json ──────────────────────────────────────────────────────
|
|
178
|
+
section("OpenCode Integration");
|
|
179
|
+
const opencodeConfigPath = join(targetDir, "opencode.json");
|
|
180
|
+
const opencodeConfig = readJsonSync(opencodeConfigPath);
|
|
181
|
+
|
|
182
|
+
if (!opencodeConfig) {
|
|
183
|
+
fail("opencode.json", "missing or invalid");
|
|
184
|
+
} else {
|
|
185
|
+
pass("opencode.json", "valid JSON");
|
|
186
|
+
|
|
187
|
+
// $schema field
|
|
188
|
+
if (opencodeConfig.$schema) {
|
|
189
|
+
pass("$schema", opencodeConfig.$schema);
|
|
190
|
+
} else {
|
|
191
|
+
softWarn("$schema", "missing (recommended: https://opencode.ai/config.json)");
|
|
192
|
+
}
|
|
63
193
|
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
194
|
+
// Root model
|
|
195
|
+
if (opencodeConfig.model) {
|
|
196
|
+
pass("Root model", opencodeConfig.model);
|
|
67
197
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
198
|
+
softWarn("Root model", "not set in opencode.json");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Instructions
|
|
202
|
+
if (Array.isArray(opencodeConfig.instructions) && opencodeConfig.instructions.length > 0) {
|
|
203
|
+
pass("Instructions", `${opencodeConfig.instructions.length} file(s)`);
|
|
204
|
+
} else {
|
|
205
|
+
softWarn("Instructions", "none configured");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Agent definitions
|
|
209
|
+
if (opencodeConfig.agent && typeof opencodeConfig.agent === "object") {
|
|
210
|
+
const agentNames = Object.keys(opencodeConfig.agent);
|
|
211
|
+
pass("Agent definitions", `${agentNames.length} agent(s)`);
|
|
212
|
+
|
|
213
|
+
// Cross-check with AGENTS constant
|
|
214
|
+
for (const role of AGENTS) {
|
|
215
|
+
const agentDef = opencodeConfig.agent[role];
|
|
216
|
+
if (!agentDef) {
|
|
217
|
+
softWarn(`Agent "${role}"`, "missing from opencode.json");
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const issues = [];
|
|
221
|
+
if (!agentDef.model) issues.push("no model");
|
|
222
|
+
if (!agentDef.mode) issues.push("no mode");
|
|
223
|
+
if (!agentDef.description) issues.push("no description");
|
|
224
|
+
if (issues.length > 0) {
|
|
225
|
+
softWarn(`Agent "${role}"`, issues.join(", "));
|
|
226
|
+
} else if (verbose) {
|
|
227
|
+
pass(`Agent "${role}"`, `${agentDef.mode} / ${agentDef.model}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check model consistency between config.json and opencode.json
|
|
232
|
+
if (config && config.models) {
|
|
233
|
+
let mismatches = 0;
|
|
234
|
+
for (const role of AGENTS) {
|
|
235
|
+
const configModel = config.models[role];
|
|
236
|
+
const ocModel = opencodeConfig.agent[role]?.model;
|
|
237
|
+
if (configModel && ocModel && configModel !== ocModel) {
|
|
238
|
+
softWarn(
|
|
239
|
+
`Model mismatch: ${role}`,
|
|
240
|
+
`config.json=${configModel} vs opencode.json=${ocModel}`
|
|
241
|
+
);
|
|
242
|
+
mismatches++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (mismatches === 0) {
|
|
246
|
+
pass("Model consistency", "config.json and opencode.json agree");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
fail("Agent definitions", "no 'agent' section in opencode.json");
|
|
70
251
|
}
|
|
71
252
|
}
|
|
72
253
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
254
|
+
// ── 6. Agent markdown files ───────────────────────────────────────────────
|
|
255
|
+
section("Agent Files");
|
|
256
|
+
const agentsDir = join(scaffoldDir, DIRS.agents);
|
|
257
|
+
|
|
258
|
+
for (const role of AGENTS) {
|
|
259
|
+
const agentFile = join(agentsDir, `${role}.md`);
|
|
260
|
+
if (!existsSync(agentFile)) {
|
|
261
|
+
fail(`Agent: ${role}`, `${DIRS.agents}/${role}.md missing`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const content = safeReadFile(agentFile);
|
|
266
|
+
if (!content) {
|
|
267
|
+
fail(`Agent: ${role}`, "empty or unreadable");
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check for YAML frontmatter
|
|
272
|
+
if (!content.startsWith("---")) {
|
|
273
|
+
softWarn(`Agent: ${role}`, "no YAML frontmatter");
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check for model field in frontmatter
|
|
278
|
+
const endIdx = content.indexOf("---", 3);
|
|
279
|
+
if (endIdx > 0) {
|
|
280
|
+
const frontmatter = content.slice(3, endIdx);
|
|
281
|
+
if (/^model:/m.test(frontmatter)) {
|
|
282
|
+
const modelMatch = frontmatter.match(/^model:\s*(.+)$/m);
|
|
283
|
+
const modelVal = modelMatch ? modelMatch[1].trim() : "?";
|
|
284
|
+
pass(`Agent: ${role}`, `model: ${modelVal}`);
|
|
285
|
+
} else {
|
|
286
|
+
softWarn(`Agent: ${role}`, "no model field in frontmatter");
|
|
287
|
+
}
|
|
76
288
|
} else {
|
|
77
|
-
|
|
78
|
-
|
|
289
|
+
softWarn(`Agent: ${role}`, "malformed frontmatter");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── 7. Rules ──────────────────────────────────────────────────────────────
|
|
294
|
+
section("Rules");
|
|
295
|
+
const rulesDir = join(scaffoldDir, DIRS.rules);
|
|
296
|
+
const universalRules = join(rulesDir, "universal.md");
|
|
297
|
+
|
|
298
|
+
if (existsSync(universalRules)) {
|
|
299
|
+
const content = safeReadFile(universalRules);
|
|
300
|
+
pass("Universal rules", content ? `${content.split("\n").length} lines` : "empty");
|
|
301
|
+
} else {
|
|
302
|
+
fail("Universal rules", `${DIRS.rules}/universal.md missing`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check for any additional rule files
|
|
306
|
+
if (existsSync(rulesDir)) {
|
|
307
|
+
const ruleFiles = safeReaddir(rulesDir).filter((f) => f.endsWith(".md") && f !== "universal.md");
|
|
308
|
+
if (ruleFiles.length > 0) {
|
|
309
|
+
pass("Custom rules", `${ruleFiles.length} additional file(s): ${ruleFiles.join(", ")}`);
|
|
79
310
|
}
|
|
80
311
|
}
|
|
81
312
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
313
|
+
// ── 8. Ralphy config ──────────────────────────────────────────────────────
|
|
314
|
+
section("Ralphy Integration");
|
|
315
|
+
const ralphyConfig = join(targetDir, ".ralphy", "config.yaml");
|
|
316
|
+
|
|
317
|
+
if (existsSync(ralphyConfig)) {
|
|
318
|
+
const content = safeReadFile(ralphyConfig);
|
|
319
|
+
pass(".ralphy/config.yaml", content ? `${content.split("\n").length} lines` : "exists");
|
|
320
|
+
|
|
321
|
+
// Check for placeholder values
|
|
322
|
+
if (content && content.includes('name: ""')) {
|
|
323
|
+
softWarn("Ralphy project name", 'still empty ("") — edit .ralphy/config.yaml');
|
|
89
324
|
}
|
|
325
|
+
} else {
|
|
326
|
+
softWarn(".ralphy/config.yaml", "not found (needed for 'run' command)");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
330
|
+
printSummary(errors, warnings);
|
|
331
|
+
|
|
332
|
+
// Return for programmatic use
|
|
333
|
+
return { errors, warnings };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
function printSummary(errors, warnings) {
|
|
339
|
+
console.log();
|
|
340
|
+
hr();
|
|
341
|
+
console.log();
|
|
342
|
+
|
|
343
|
+
if (errors === 0 && warnings === 0) {
|
|
344
|
+
success("All checks passed. Your project is ready.");
|
|
345
|
+
} else if (errors === 0) {
|
|
346
|
+
console.log(` ${c.bgYellow(" RESULT ")} ${c.yellow(`${warnings} warning(s), 0 errors`)}`);
|
|
347
|
+
console.log(` ${c.gray("Warnings won't prevent execution but may indicate issues.")}`);
|
|
348
|
+
console.log();
|
|
349
|
+
} else {
|
|
350
|
+
console.log(` ${c.bgRed(" RESULT ")} ${c.red(`${errors} error(s), ${warnings} warning(s)`)}`);
|
|
351
|
+
console.log(` ${c.gray("Fix the errors above before running the agent team.")}`);
|
|
352
|
+
console.log();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function padRole(role) {
|
|
357
|
+
return role.padEnd(14);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function safeReaddir(dir) {
|
|
361
|
+
try {
|
|
362
|
+
return readdirSync(dir);
|
|
363
|
+
} catch {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function safeReadFile(filePath) {
|
|
369
|
+
try {
|
|
370
|
+
return readFileSync(filePath, "utf8");
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
90
373
|
}
|
|
91
374
|
}
|