diff --git a/src/tools/delegate-task/subagent-discovery.ts b/src/tools/delegate-task/subagent-discovery.ts index 340ef7c22..118d3adf8 100644 --- a/src/tools/delegate-task/subagent-discovery.ts +++ b/src/tools/delegate-task/subagent-discovery.ts @@ -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,10 +21,11 @@ export function mergeWithClaudeCodeAgents( const userAgentsRecord = loadUserAgents() const projectAgentsRecord = loadProjectAgents(directory) - const toAgentInfoList = (record: Record): AgentInfo[] => + const toAgentInfoList = (record: Record): AgentInfo[] => Object.entries(record).map(([name, config]) => ({ name, mode: config.mode as AgentInfo["mode"], + hidden: config.hidden, model: config.model, })) @@ -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(", ") diff --git a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts index 05e988716..343177ace 100644 --- a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts +++ b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts @@ -248,6 +248,61 @@ 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("normalizes matched agent model string before returning categoryModel", async () => { //#given readProviderModelsCacheMock.mockReturnValue({