Merge pull request #3992 from MoerAI/fix/omo-block-native-execution-delegation

fix(delegate-task): exclude hidden agents from task delegation discovery (fixes #3957)
This commit is contained in:
YeonGyu-Kim
2026-05-13 23:44:50 +09:00
committed by GitHub
2 changed files with 133 additions and 4 deletions
@@ -6,6 +6,7 @@ export type AgentMode = "subagent" | "primary" | "all" | undefined
export type AgentInfo = {
name: string
mode?: "subagent" | "primary" | "all"
hidden?: boolean
model?: string | { providerID: string; modelID: string }
}
@@ -20,16 +21,17 @@ export function mergeWithClaudeCodeAgents(
const userAgentsRecord = loadUserAgents()
const projectAgentsRecord = loadProjectAgents(directory)
const toAgentInfoList = (record: Record<string, { mode?: string; model?: AgentInfo["model"] }>): AgentInfo[] =>
const toAgentInfoList = (record: Record<string, { mode?: string; hidden?: boolean; model?: AgentInfo["model"] }>): AgentInfo[] =>
Object.entries(record).map(([name, config]) => ({
name,
mode: config.mode as AgentInfo["mode"],
hidden: config.hidden,
model: config.model,
}))
const mergedAgentMap = new Map<string, AgentInfo>()
const addIfAbsent = (agent: AgentInfo): void => {
const key = agent.name.toLowerCase()
const key = stripAgentListSortPrefix(agent.name).trim().toLowerCase()
if (!mergedAgentMap.has(key)) {
mergedAgentMap.set(key, agent)
}
@@ -73,12 +75,12 @@ export function findCallableAgentMatch(
agents: AgentInfo[],
requestedAgentName: string,
): AgentInfo | undefined {
return agents.find(agent => isTaskCallableAgentMode(agent.mode) && matchesRequestedAgent(agent, requestedAgentName))
return agents.find(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true && matchesRequestedAgent(agent, requestedAgentName))
}
export function listCallableAgentNames(agents: AgentInfo[]): string {
return agents
.filter(agent => isTaskCallableAgentMode(agent.mode))
.filter(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true)
.map(agent => stripAgentListSortPrefix(agent.name))
.sort()
.join(", ")
@@ -248,6 +248,133 @@ describe("resolveSubagentExecution", () => {
expect(result.error).toBe('Unknown agent: "custom-worker". Available agents: oracle')
})
test("rejects delegation to hidden native execution agents (regression #3957)", async () => {
//#given
const args = createBaseArgs({ subagent_type: "build" })
const executorCtx = createExecutorContext(async () => ([
{ name: "build", mode: "subagent", hidden: true },
{ name: "oracle", mode: "subagent" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.categoryModel).toBeUndefined()
expect(result.error).toBe('Unknown agent: "build". Available agents: oracle')
})
test("rejects delegation to hidden plan agent demoted to subagent (regression #3957)", async () => {
//#given
const args = createBaseArgs({ subagent_type: "plan" })
const executorCtx = createExecutorContext(async () => ([
{ name: "plan", mode: "subagent", hidden: true },
{ name: "oracle", mode: "subagent" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.categoryModel).toBeUndefined()
expect(result.error).toBe('Unknown agent: "plan". Available agents: oracle')
})
test("hidden agents are excluded from listCallableAgentNames in error messages (regression #3957)", async () => {
//#given
const args = createBaseArgs({ subagent_type: "nonexistent" })
const executorCtx = createExecutorContext(async () => ([
{ name: "build", mode: "subagent", hidden: true },
{ name: "plan", mode: "subagent", hidden: true },
{ name: "oracle", mode: "subagent" },
{ name: "explore", mode: "subagent" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.error).toBeDefined()
expect(result.error).toContain('Available agents: explore, oracle')
expect(result.error).not.toContain("build")
expect(result.error).not.toContain("plan")
})
test("rejects ZWSP-prefixed project agent that canonicalizes to hidden build (regression #3957 canonical-key bypass)", async () => {
//#given
loadProjectAgentsMock.mockImplementation(() => ({
"\u200Bbuild": {
description: "Aliases hidden build via zero-width prefix",
mode: "subagent",
prompt: "rogue",
},
}))
const args = createBaseArgs({ subagent_type: "build" })
const executorCtx = createExecutorContext(async () => ([
{ name: "build", mode: "subagent", hidden: true },
{ name: "oracle", mode: "subagent" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.categoryModel).toBeUndefined()
expect(result.error).toBe('Unknown agent: "build". Available agents: oracle')
})
test("rejects quoted user agent that canonicalizes to hidden plan (regression #3957 canonical-key bypass)", async () => {
//#given
loadUserAgentsMock.mockImplementation(() => ({
'"plan"': {
description: "Aliases hidden plan via quote wrappers",
mode: "subagent",
prompt: "rogue",
},
}))
const args = createBaseArgs({ subagent_type: "plan" })
const executorCtx = createExecutorContext(async () => ([
{ name: "plan", mode: "subagent", hidden: true },
{ name: "oracle", mode: "subagent" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.categoryModel).toBeUndefined()
expect(result.error).toBe('Unknown agent: "plan". Available agents: oracle')
})
test("rejects sort-prefixed project agent that canonicalizes to hidden build (regression #3957 canonical-key bypass)", async () => {
//#given
loadProjectAgentsMock.mockImplementation(() => ({
"1|build": {
description: "Aliases hidden build via sort prefix",
mode: "subagent",
prompt: "rogue",
},
}))
const args = createBaseArgs({ subagent_type: "build" })
const executorCtx = createExecutorContext(async () => ([
{ name: "build", mode: "subagent", hidden: true },
{ name: "oracle", mode: "subagent" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.categoryModel).toBeUndefined()
expect(result.error).toBe('Unknown agent: "build". Available agents: oracle')
})
test("normalizes matched agent model string before returning categoryModel", async () => {
//#given
readProviderModelsCacheMock.mockReturnValue({