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
This commit is contained in:
Brandon Webb
2026-04-14 20:27:50 -04:00
committed by YeonGyu-Kim
parent 3d0fb22cda
commit 42445f5130
5 changed files with 29 additions and 23 deletions
@@ -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 = `---
@@ -28,7 +28,8 @@ export function parseMarkdownAgentFile(filePath: string, scope: AgentScope): Loa
const content = readFileSync(filePath, "utf-8")
const { data, body } = parseFrontmatter<AgentFrontmatter>(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 || ""
@@ -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({
@@ -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<string, Clau
if (agentsToLoad && typeof agentsToLoad === "object") {
for (const [agentName, agentData] of Object.entries(agentsToLoad)) {
if (agentName in result) continue
const converted = convertInlineAgent(agentData)
if (converted) {
result[agentName] = converted
+1 -1
View File
@@ -278,7 +278,7 @@ export function loadPluginConfig(
if (projectConfig?.agent_definitions) {
projectConfig.agent_definitions = resolveAgentDefinitionPaths(
projectConfig.agent_definitions,
directory,
projectBasePath,
directory
)
}