diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index a2cad5bd874..882a1cd293a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -108,6 +108,12 @@ export namespace Config { )), ] + log.debug("config directories discovered", { + directories, + instanceDirectory: Instance.directory, + worktree: Instance.worktree, + }) + if (Flag.OPENCODE_CONFIG_DIR) { directories.push(Flag.OPENCODE_CONFIG_DIR) log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) @@ -129,10 +135,25 @@ export namespace Config { const installing = installDependencies(dir) if (!exists) await installing - result.command = mergeDeep(result.command ?? {}, await loadCommand(dir)) - result.agent = mergeDeep(result.agent, await loadAgent(dir)) - result.agent = mergeDeep(result.agent, await loadMode(dir)) - result.plugin.push(...(await loadPlugin(dir))) + const commands = await loadCommand(dir) + const agents = await loadAgent(dir) + const modes = await loadMode(dir) + const plugins = await loadPlugin(dir) + + log.debug("loaded config from directory", { + dir, + commandCount: Object.keys(commands).length, + agentCount: Object.keys(agents).length, + modeCount: Object.keys(modes).length, + pluginCount: plugins.length, + commandNames: Object.keys(commands), + agentNames: Object.keys(agents), + }) + + result.command = mergeDeep(result.command ?? {}, commands) + result.agent = mergeDeep(result.agent, agents) + result.agent = mergeDeep(result.agent, modes) + result.plugin.push(...plugins) } // Migrate deprecated mode field to agent field diff --git a/packages/opencode/test/config/issue-8868.test.ts b/packages/opencode/test/config/issue-8868.test.ts new file mode 100644 index 00000000000..335470e62d8 --- /dev/null +++ b/packages/opencode/test/config/issue-8868.test.ts @@ -0,0 +1,187 @@ +import { test, expect, describe } from "bun:test" +import { Config } from "../../src/config/config" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" +import path from "path" +import fs from "fs/promises" + +/** + * Test case designed to reproduce GitHub Issue #8868 + * https://github.com/anomalyco/opencode/issues/8868 + * + * The user reports that agents and commands in `.opencode/agent/` and + * `.opencode/command/` directories are not being loaded when: + * - There's a top-level `opencode.json` with MCP config + * - Agents/commands are `.md` files in `.opencode/{agent,command}/` + * + * Interestingly, renaming `opencode.json` to `opencode.jsonc` temporarily + * makes them appear, suggesting a file-watcher or reload issue. + */ +describe("Issue #8868 - Agents and Commands not shown", () => { + test("loads agents from .opencode/agent when opencode.json exists at project root", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // Create top-level opencode.json (like the user has) + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + mcp: { + "github-mcp-server": { + type: "remote", + url: "https://api.githubcopilot.com/mcp/", + }, + }, + }), + ) + + // Create .opencode directory structure + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + // Create agent directory with a test agent + const agentDir = path.join(opencodeDir, "agent") + await fs.mkdir(agentDir, { recursive: true }) + + await Bun.write( + path.join(agentDir, "test-orchestrator.md"), + `--- +model: github-copilot/gpt-4 +mode: primary +description: Test orchestrator agent +--- +You are a test orchestrator agent.`, + ) + + // Create command directory with a test command + const commandDir = path.join(opencodeDir, "command") + await fs.mkdir(commandDir, { recursive: true }) + + await Bun.write( + path.join(commandDir, "test-analyze.md"), + `--- +description: Test analyze command +--- +Analyze the following: $ARGUMENTS`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + + // Verify the agent was loaded from .opencode/agent/ + expect(config.agent?.["test-orchestrator"]).toBeDefined() + expect(config.agent?.["test-orchestrator"]).toMatchObject({ + name: "test-orchestrator", + mode: "primary", + description: "Test orchestrator agent", + prompt: "You are a test orchestrator agent.", + }) + + // Verify the command was loaded from .opencode/command/ + expect(config.command?.["test-analyze"]).toBeDefined() + expect(config.command?.["test-analyze"]).toMatchObject({ + description: "Test analyze command", + template: "Analyze the following: $ARGUMENTS", + }) + }, + }) + }) + + test("loads agents from .opencode/agent when opencode.jsonc exists at project root", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // Create top-level opencode.jsonc (the workaround extension) + await Bun.write( + path.join(dir, "opencode.jsonc"), + `{ + // This is the JSONC file that the user reports works temporarily + "$schema": "https://opencode.ai/config.json", + "mcp": { + "github-mcp-server": { + "type": "remote", + "url": "https://api.githubcopilot.com/mcp/" + } + } +}`, + ) + + // Create .opencode directory structure + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + // Create agent directory with a test agent + const agentDir = path.join(opencodeDir, "agent") + await fs.mkdir(agentDir, { recursive: true }) + + await Bun.write( + path.join(agentDir, "jsonc-agent.md"), + `--- +model: test/model +mode: subagent +--- +JSONC test agent prompt`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + + // Verify the agent was loaded + expect(config.agent?.["jsonc-agent"]).toBeDefined() + expect(config.agent?.["jsonc-agent"]).toMatchObject({ + name: "jsonc-agent", + mode: "subagent", + prompt: "JSONC test agent prompt", + }) + }, + }) + }) + + test("loads agents with nested directory structure in .opencode/agents", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + + const opencodeDir = path.join(dir, ".opencode") + const agentsDir = path.join(opencodeDir, "agents") + const nestedDir = path.join(agentsDir, "speckit") + await fs.mkdir(nestedDir, { recursive: true }) + + await Bun.write( + path.join(nestedDir, "analyzer.md"), + `--- +mode: subagent +--- +Nested analyzer agent`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + + // The agent should be named with the nested path + expect(config.agent?.["speckit/analyzer"]).toBeDefined() + expect(config.agent?.["speckit/analyzer"]).toMatchObject({ + name: "speckit/analyzer", + mode: "subagent", + prompt: "Nested analyzer agent", + }) + }, + }) + }) +})