From 42445f513029c6f7cb06090cc236f0d7d77c38f6 Mon Sep 17 00:00:00 2001 From: Brandon Webb Date: Tue, 14 Apr 2026 20:27:50 -0400 Subject: [PATCH] fix(agents): address cubic review findings on agent loader - Case-insensitive .md extension stripping for agent name extraction - Resolve project agent_definitions paths relative to config dir (.opencode/) - Use getOpenCodeConfigDir() to respect OPENCODE_CONFIG_DIR/XDG_CONFIG_HOME - First-write-wins semantics for both inline and definition-file agents so project-level agents always take precedence over global-level --- .../agent-definitions-loader.test.ts | 16 ++++++++++++++ .../agent-definitions-loader.ts | 3 ++- .../opencode-config-agents-reader.test.ts | 10 ++++----- .../opencode-config-agents-reader.ts | 21 +++++-------------- src/plugin-config.ts | 2 +- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/features/claude-code-agent-loader/agent-definitions-loader.test.ts b/src/features/claude-code-agent-loader/agent-definitions-loader.test.ts index 3ce8d8521..76960b93a 100644 --- a/src/features/claude-code-agent-loader/agent-definitions-loader.test.ts +++ b/src/features/claude-code-agent-loader/agent-definitions-loader.test.ts @@ -82,6 +82,22 @@ Prompt.` expect(result?.config.prompt).toBe("Prompt.") }) + test("strips .MD extension case-insensitively for agent name", () => { + const filePath = join(tempDir, "UpperCase.MD") + const content = `--- +description: Mixed case extension +--- + +Prompt content.` + + writeFileSync(filePath, content, "utf-8") + + const result = parseMarkdownAgentFile(filePath, "definition-file") + + expect(result).not.toBeNull() + expect(result?.name).toBe("UpperCase") + }) + test("defaults mode to subagent when not specified", () => { const filePath = join(tempDir, "no-mode.md") const content = `--- diff --git a/src/features/claude-code-agent-loader/agent-definitions-loader.ts b/src/features/claude-code-agent-loader/agent-definitions-loader.ts index b541e5f88..0736fe347 100644 --- a/src/features/claude-code-agent-loader/agent-definitions-loader.ts +++ b/src/features/claude-code-agent-loader/agent-definitions-loader.ts @@ -28,7 +28,8 @@ export function parseMarkdownAgentFile(filePath: string, scope: AgentScope): Loa const content = readFileSync(filePath, "utf-8") const { data, body } = parseFrontmatter(content) - const agentName = basename(filePath, ".md") + const fileName = basename(filePath) + const agentName = fileName.replace(/\.md$/i, "") const name = data.name || agentName const originalDescription = data.description || "" diff --git a/src/features/claude-code-agent-loader/opencode-config-agents-reader.test.ts b/src/features/claude-code-agent-loader/opencode-config-agents-reader.test.ts index 1ecc77e63..52ed83da3 100644 --- a/src/features/claude-code-agent-loader/opencode-config-agents-reader.test.ts +++ b/src/features/claude-code-agent-loader/opencode-config-agents-reader.test.ts @@ -124,7 +124,7 @@ describe("readOpencodeConfigAgents", () => { const opencodeDir = path.join(tempDir, ".opencode") fs.mkdirSync(opencodeDir, { recursive: true }) - const agentDefFile = path.join(tempDir, "agents.json") + const agentDefFile = path.join(opencodeDir, "agents.json") fs.writeFileSync( agentDefFile, JSON.stringify({ @@ -146,7 +146,7 @@ describe("readOpencodeConfigAgents", () => { if (Object.keys(result).length > 0) { expect(result).toHaveProperty("definition-agent") - expect(result["definition-agent"].description).toContain("definition-file") + expect(result["definition-agent"].description).toContain("From definition file") } fs.rmSync(tempDir, { recursive: true }) @@ -157,7 +157,7 @@ describe("readOpencodeConfigAgents", () => { const opencodeDir = path.join(tempDir, ".opencode") fs.mkdirSync(opencodeDir, { recursive: true }) - const agentDefFile = path.join(tempDir, "agents.json") + const agentDefFile = path.join(opencodeDir, "agents.json") fs.writeFileSync( agentDefFile, JSON.stringify({ @@ -288,7 +288,7 @@ describe("readOpencodeConfigAgents", () => { const opencodeDir = path.join(tempDir, ".opencode") fs.mkdirSync(opencodeDir, { recursive: true }) - const agentDef1 = path.join(tempDir, "agents1.json") + const agentDef1 = path.join(opencodeDir, "agents1.json") fs.writeFileSync( agentDef1, JSON.stringify({ @@ -298,7 +298,7 @@ describe("readOpencodeConfigAgents", () => { }) ) - const agentDef2 = path.join(tempDir, "agents2.json") + const agentDef2 = path.join(opencodeDir, "agents2.json") fs.writeFileSync( agentDef2, JSON.stringify({ diff --git a/src/features/claude-code-agent-loader/opencode-config-agents-reader.ts b/src/features/claude-code-agent-loader/opencode-config-agents-reader.ts index 5fcb75cea..2859a361c 100644 --- a/src/features/claude-code-agent-loader/opencode-config-agents-reader.ts +++ b/src/features/claude-code-agent-loader/opencode-config-agents-reader.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs" -import * as os from "node:os" import * as path from "node:path" +import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir" import { parseJsoncSafe } from "../../shared/jsonc-parser" import { loadAgentDefinitions } from "./agent-definitions-loader" import { mapClaudeModelToOpenCode } from "./claude-model-mapper" @@ -13,27 +13,15 @@ interface OpencodeConfigWithAgents { agent_definitions?: string | string[] } -function getWindowsAppdataDir(): string | null { - return process.env.APPDATA || null -} - function getConfigPaths(directory: string): string[] { - const crossPlatformDir = path.join(os.homedir(), ".config") + const globalConfigDir = getOpenCodeConfigDir({ binary: "opencode" }) const paths = [ path.join(directory, ".opencode", "opencode.json"), path.join(directory, ".opencode", "opencode.jsonc"), - path.join(crossPlatformDir, "opencode", "opencode.json"), - path.join(crossPlatformDir, "opencode", "opencode.jsonc"), + path.join(globalConfigDir, "opencode.json"), + path.join(globalConfigDir, "opencode.jsonc"), ] - if (process.platform === "win32") { - const appdataDir = getWindowsAppdataDir() - if (appdataDir) { - paths.push(path.join(appdataDir, "opencode", "opencode.json")) - paths.push(path.join(appdataDir, "opencode", "opencode.jsonc")) - } - } - return paths } @@ -108,6 +96,7 @@ export function readOpencodeConfigAgents(directory: string): Record