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,91 +1,374 @@
1
1
  /**
2
- * doctor command — validate configuration and tool availability.
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 { SCAFFOLD_DIR, CONFIG_FILE, DIRS, AGENTS, PRESETS } from "../lib/constants.js";
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
- console.log("[doctor] Checking opencode-auto-agent configuration...\n");
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
- // 1. Scaffold directory exists
20
- check("Scaffold directory", existsSync(scaffoldDir), `${SCAFFOLD_DIR}/ not found. Run 'opencode-auto-agent init'.`);
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
- // 2. Config file
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
- // 3. Preset is valid
29
- const presetFile = join(scaffoldDir, DIRS.presets, `${config.preset}.md`);
30
- check(`Preset "${config.preset}"`, existsSync(presetFile), `Preset file not found: presets/${config.preset}.md`);
94
+ if (!config) {
95
+ fail(CONFIG_FILE, "missing or invalid JSON");
96
+ } else {
97
+ pass(CONFIG_FILE, "valid JSON");
31
98
 
32
- // 4. Task file exists
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
- warn(`Tasks file (${config.tasksPath || "PRD.md"})`, existsSync(tasksPath), "Create a PRD.md or update tasksPath in config.");
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
- // 5. Agent markdowns
38
- for (const agent of AGENTS) {
39
- const agentFile = join(scaffoldDir, DIRS.agents, `${agent}.md`);
40
- check(`Agent: ${agent}`, existsSync(agentFile), `Missing agent definition: agents/${agent}.md`);
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
- // 6. Rules
44
- const rulesDir = join(scaffoldDir, DIRS.rules, "universal.md");
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
- // 8. OpenCode config
52
- const ocConfig = join(targetDir, "opencode.json");
53
- warn("opencode.json", existsSync(ocConfig), "Run 'opencode-auto-agent init' to generate it.");
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
- console.log("\n[doctor] ─────────────────────────────────────────");
56
- if (errors === 0 && warnings === 0) {
57
- console.log("[doctor] All checks passed.");
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
- console.log(`[doctor] ${errors} error(s), ${warnings} warning(s).`);
174
+ softWarn("Model validation", "skipped — no models mapping in config");
60
175
  }
61
176
 
62
- // ── helpers ──────────────────────────────────────────────────────────────
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
- function check(label, ok, hint) {
65
- if (ok) {
66
- console.log(` [pass] ${label}`);
194
+ // Root model
195
+ if (opencodeConfig.model) {
196
+ pass("Root model", opencodeConfig.model);
67
197
  } else {
68
- console.log(` [FAIL] ${label} ${hint}`);
69
- errors++;
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
- function warn(label, ok, hint) {
74
- if (ok) {
75
- console.log(` [pass] ${label}`);
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
- console.log(` [warn] ${label} ${hint}`);
78
- warnings++;
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
- function checkTool(name, installHint) {
83
- try {
84
- execSync(`${name} --version`, { stdio: "pipe" });
85
- console.log(` [pass] CLI: ${name}`);
86
- } catch {
87
- console.log(` [warn] CLI: ${name} not found. Install: ${installHint}`);
88
- warnings++;
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
  }