From 43c6d820f6995726a5c9cd6dba9687d6c750e579 Mon Sep 17 00:00:00 2001 From: Hardik Sharma Date: Wed, 29 Jul 2026 18:20:31 +0530 Subject: [PATCH 1/3] fix(cli): preserve configured subagent routing --- .changeset/fair-subagents-route.md | 5 ++ packages/opencode/src/config/config.ts | 4 +- .../opencode/src/kilocode/config/config.ts | 28 +++++++ .../test/kilocode/agent-routing.test.ts | 84 +++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 .changeset/fair-subagents-route.md create mode 100644 packages/opencode/test/kilocode/agent-routing.test.ts diff --git a/.changeset/fair-subagents-route.md b/.changeset/fair-subagents-route.md new file mode 100644 index 00000000000..a1d2775fc90 --- /dev/null +++ b/.changeset/fair-subagents-route.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep config-defined subagents routable when an installed primary agent uses the same name. diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 8c396e638a2..d51ed2e0aa0 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -758,11 +758,11 @@ export const layer = Layer.effect( result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)), ) - result.agent = mergeDeep( + result.agent = KilocodeConfig.mergeAgentMarkdown( result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)), ) - result.agent = mergeDeep( + result.agent = KilocodeConfig.mergeAgentMarkdown( result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)), ) diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts index a33445494b8..5d63a489996 100644 --- a/packages/opencode/src/kilocode/config/config.ts +++ b/packages/opencode/src/kilocode/config/config.ts @@ -112,6 +112,34 @@ export namespace KilocodeConfig { return stripGlobalIndexing(info) } + /** + * Merge discovered agent markdown without silently reclassifying an earlier + * config-defined subagent as a primary agent. A config-only custom agent has + * the normal default mode of "all", even when a later markdown definition + * shares its name. + */ + export function mergeAgentMarkdown( + existing: Record, + incoming: Record, + ) { + const result = { ...existing } + for (const [name, agent] of Object.entries(incoming)) { + const current = result[name] + if (!current) { + result[name] = agent + continue + } + + if (agent.mode === "primary" && current.mode !== "primary") { + result[name] = mergeDeep(agent, { ...current, mode: current.mode ?? "all" }) + continue + } + + result[name] = mergeDeep(current, agent) + } + return result + } + export function retireIndexingFlag(info: Record, source: string) { if (!isRecord(info.experimental) || !("semantic_indexing" in info.experimental)) return info const experimental = { ...info.experimental } diff --git a/packages/opencode/test/kilocode/agent-routing.test.ts b/packages/opencode/test/kilocode/agent-routing.test.ts new file mode 100644 index 00000000000..b8ea46f7612 --- /dev/null +++ b/packages/opencode/test/kilocode/agent-routing.test.ts @@ -0,0 +1,84 @@ +import { afterEach, expect, test } from "bun:test" +import path from "path" +import { Effect } from "effect" +import { Agent } from "../../src/agent/agent" +import { Filesystem } from "../../src/util/filesystem" +import { + disposeAllInstances, + provideInstance, + provideTestInstance, + testInstanceStoreLayer, + tmpdir, +} from "../fixture/fixture" + +function load(dir: string) { + return Effect.runPromise( + provideInstance(dir)(Agent.Service.use((svc) => svc.get("architect"))).pipe( + Effect.provide(Agent.defaultLayer), + Effect.provide(testInstanceStoreLayer), + ), + ) +} + +afterEach(async () => { + await disposeAllInstances() +}) + +test("config subagent routing survives a colliding primary agent markdown file", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + architect: { + mode: "subagent", + model: "test/configured-subagent", + }, + }, + }, + }) + await Filesystem.write( + path.join(tmp.path, ".kilo", "agents", "architect.md"), + [ + "---", + "mode: primary", + "description: Marketplace architect", + "---", + "", + "You are the marketplace architect.", + ].join("\n"), + ) + + const item = await provideTestInstance({ + directory: tmp.path, + fn: () => load(tmp.path), + }) + + expect(item?.mode).toBe("subagent") + expect(String(item?.model?.providerID)).toBe("test") + expect(String(item?.model?.modelID)).toBe("configured-subagent") + expect(item?.description).toBe("Marketplace architect") +}) + +test("config-only custom agent keeps its default all mode across a primary collision", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + architect: { + model: "test/configured-subagent", + }, + }, + }, + }) + await Filesystem.write( + path.join(tmp.path, ".kilo", "agents", "architect.md"), + ["---", "mode: primary", "---", "", "You are the marketplace architect."].join("\n"), + ) + + const item = await provideTestInstance({ + directory: tmp.path, + fn: () => load(tmp.path), + }) + + expect(item?.mode).toBe("all") + expect(String(item?.model?.providerID)).toBe("test") + expect(String(item?.model?.modelID)).toBe("configured-subagent") +}) From ae17deaaa87b6e82be61df6b9d6ad645709bf9bd Mon Sep 17 00:00:00 2001 From: Hardik Sharma Date: Wed, 29 Jul 2026 18:57:31 +0530 Subject: [PATCH 2/3] fix(cli): preserve markdown agent precedence --- packages/opencode/src/config/config.ts | 4 +++ .../opencode/src/kilocode/config/config.ts | 13 +++++---- .../test/kilocode/agent-routing.test.ts | 29 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index d51ed2e0aa0..48996b34735 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -483,6 +483,7 @@ export const layer = Layer.effect( result = mergeConfigConcatArrays(result, { agent: orgModes.agents }) } warnings.push(...orgModes.warnings) + let configuredAgents = { ...(result.agent ?? {}) } // kilocode_change end const authEnv: Record = {} @@ -542,6 +543,7 @@ export const layer = Layer.effect( const trusted = sourceTrusted ?? scope === "global" const scoped = KilocodeConfig.scopeIndexing(SandboxConfig.scope(next, scope), scope) result = mergeConfigConcatArrays(result, scoped) + if (scoped.agent) configuredAgents = mergeDeep(configuredAgents, scoped.agent) if (next.instructions?.length) { result.instruction_origins = origins(result.instruction_origins, next.instructions, trusted, source) } @@ -761,10 +763,12 @@ export const layer = Layer.effect( result.agent = KilocodeConfig.mergeAgentMarkdown( result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)), + configuredAgents, ) result.agent = KilocodeConfig.mergeAgentMarkdown( result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)), + configuredAgents, ) // kilocode_change end // kilocode_change - Auto-discovered plugins under config directories are already local files, so ConfigPlugin.load diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts index 5d63a489996..88a0f94c9c2 100644 --- a/packages/opencode/src/kilocode/config/config.ts +++ b/packages/opencode/src/kilocode/config/config.ts @@ -113,14 +113,14 @@ export namespace KilocodeConfig { } /** - * Merge discovered agent markdown without silently reclassifying an earlier - * config-defined subagent as a primary agent. A config-only custom agent has - * the normal default mode of "all", even when a later markdown definition - * shares its name. + * Merge discovered agent markdown while preserving routing explicitly defined + * in config. Tracking config entries separately keeps normal directory + * precedence between markdown files intact. */ export function mergeAgentMarkdown( existing: Record, incoming: Record, + configured: Record, ) { const result = { ...existing } for (const [name, agent] of Object.entries(incoming)) { @@ -130,8 +130,9 @@ export namespace KilocodeConfig { continue } - if (agent.mode === "primary" && current.mode !== "primary") { - result[name] = mergeDeep(agent, { ...current, mode: current.mode ?? "all" }) + const config = configured[name] + if (agent.mode === "primary" && config && config.mode !== "primary") { + result[name] = mergeDeep(mergeDeep(current, agent), { ...config, mode: config.mode ?? "all" }) continue } diff --git a/packages/opencode/test/kilocode/agent-routing.test.ts b/packages/opencode/test/kilocode/agent-routing.test.ts index b8ea46f7612..2ce79e53423 100644 --- a/packages/opencode/test/kilocode/agent-routing.test.ts +++ b/packages/opencode/test/kilocode/agent-routing.test.ts @@ -1,4 +1,5 @@ import { afterEach, expect, test } from "bun:test" +import { Global } from "@opencode-ai/core/global" import path from "path" import { Effect } from "effect" import { Agent } from "../../src/agent/agent" @@ -82,3 +83,31 @@ test("config-only custom agent keeps its default all mode across a primary colli expect(String(item?.model?.providerID)).toBe("test") expect(String(item?.model?.modelID)).toBe("configured-subagent") }) + +test("higher-priority markdown can override lower-priority markdown routing", async () => { + await using tmp = await tmpdir() + const project = path.join(tmp.path, "project") + const global = path.join(tmp.path, "global") + await Filesystem.write( + path.join(global, "agents", "architect.md"), + ["---", "mode: subagent", "description: Lower-priority architect", "---"].join("\n"), + ) + await Filesystem.write( + path.join(project, ".kilo", "agents", "architect.md"), + ["---", "mode: primary", "description: Higher-priority architect", "---"].join("\n"), + ) + + const previous = Global.Path.config + ;(Global.Path as { config: string }).config = global + try { + const item = await provideTestInstance({ + directory: project, + fn: () => load(project), + }) + + expect(item?.mode).toBe("primary") + expect(item?.description).toBe("Higher-priority architect") + } finally { + ;(Global.Path as { config: string }).config = previous + } +}) From 31dd8f7e8f8902785bc0261a151265df9da8cc10 Mon Sep 17 00:00:00 2001 From: Hardik Sharma Date: Sun, 2 Aug 2026 02:41:25 +0530 Subject: [PATCH 3/3] ci: rerun checks