fix(cli): preserve configured subagent routing (#12652)

* fix(cli): preserve configured subagent routing

* fix(cli): preserve markdown agent precedence

* ci: rerun checks
This commit is contained in:
Hardik Sharma
2026-08-01 21:19:19 +00:00
committed by GitHub
parent f08b78c7a2
commit c554409080
4 changed files with 153 additions and 2 deletions
+6 -2
View File
@@ -490,6 +490,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<string, string> = {}
@@ -549,6 +550,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)
}
@@ -765,13 +767,15 @@ 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)),
configuredAgents,
)
result.agent = mergeDeep(
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
@@ -181,6 +181,35 @@ export namespace KilocodeConfig {
return stripGlobalIndexing(info)
}
/**
* 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<string, ConfigAgentV1.Info>,
incoming: Record<string, ConfigAgentV1.Info>,
configured: Record<string, ConfigAgentV1.Info>,
) {
const result = { ...existing }
for (const [name, agent] of Object.entries(incoming)) {
const current = result[name]
if (!current) {
result[name] = agent
continue
}
const config = configured[name]
if (agent.mode === "primary" && config && config.mode !== "primary") {
result[name] = mergeDeep(mergeDeep(current, agent), { ...config, mode: config.mode ?? "all" })
continue
}
result[name] = mergeDeep(current, agent)
}
return result
}
export function retireIndexingFlag(info: Record<string, unknown>, source: string) {
if (!isRecord(info.experimental) || !("semantic_indexing" in info.experimental)) return info
const experimental = { ...info.experimental }
@@ -0,0 +1,113 @@
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"
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")
})
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
}
})