Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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
Expand Down
187 changes: 187 additions & 0 deletions packages/opencode/test/config/issue-8868.test.ts
Original file line number Diff line number Diff line change
@@ -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",
})
},
})
})
})