diff --git a/src/tools/delegate-task/subagent-discovery.ts b/src/tools/delegate-task/subagent-discovery.ts index fc67432e3..46e2f00e4 100644 --- a/src/tools/delegate-task/subagent-discovery.ts +++ b/src/tools/delegate-task/subagent-discovery.ts @@ -1,4 +1,9 @@ -import { getAgentConfigKey, getAgentDisplayName, stripAgentListSortPrefix } from "../../shared/agent-display-names" +import { + getAgentConfigKey, + getAgentDisplayName, + stripAgentListSortPrefix, + stripInvisibleAgentCharacters, +} from "../../shared/agent-display-names" import { loadUserAgents, loadProjectAgents } from "../../features/claude-code-agent-loader" export type AgentMode = "subagent" | "primary" | "all" | undefined @@ -64,6 +69,16 @@ export function isTaskCallableAgentMode(mode: AgentMode): boolean { return mode === "all" || mode === "subagent" } +function isDemotedPlanAgent(agent: AgentInfo): boolean { + return agent.hidden === true + && agent.mode === "subagent" + && stripInvisibleAgentCharacters(agent.name).trim().toLowerCase() === "plan" +} + +function isVisibleToTask(agent: AgentInfo): boolean { + return agent.hidden !== true || isDemotedPlanAgent(agent) +} + export function findPrimaryAgentMatch( agents: AgentInfo[], requestedAgentName: string, @@ -75,12 +90,12 @@ export function findCallableAgentMatch( agents: AgentInfo[], requestedAgentName: string, ): AgentInfo | undefined { - return agents.find(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true && matchesRequestedAgent(agent, requestedAgentName)) + return agents.find(agent => isTaskCallableAgentMode(agent.mode) && isVisibleToTask(agent) && matchesRequestedAgent(agent, requestedAgentName)) } export function listCallableAgentNames(agents: AgentInfo[]): string { return agents - .filter(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true) + .filter(agent => isTaskCallableAgentMode(agent.mode) && isVisibleToTask(agent)) .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 39f3e3f16..6a9fe65e6 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 @@ -265,7 +265,7 @@ describe("resolveSubagentExecution", () => { expect(result.error).toBe('Unknown agent: "build". Available agents: oracle') }) - test("rejects delegation to hidden plan agent demoted to subagent (regression #3957)", async () => { + test("allows delegation to hidden plan agent demoted to subagent", async () => { //#given const args = createBaseArgs({ subagent_type: "plan" }) const executorCtx = createExecutorContext(async () => ([ @@ -277,12 +277,12 @@ describe("resolveSubagentExecution", () => { const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") //#then - expect(result.agentToUse).toBe("") + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("plan") 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 () => { + test("hidden agents are excluded from error hints except callable demoted plan", async () => { //#given const args = createBaseArgs({ subagent_type: "nonexistent" }) const executorCtx = createExecutorContext(async () => ([ @@ -298,9 +298,8 @@ describe("resolveSubagentExecution", () => { //#then expect(result.agentToUse).toBe("") expect(result.error).toBeDefined() - expect(result.error).toContain('Available agents: explore, oracle') + expect(result.error).toContain('Available agents: explore, oracle, plan') 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 () => { @@ -327,7 +326,7 @@ describe("resolveSubagentExecution", () => { 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 () => { + test("uses built-in hidden plan instead of quoted user agent alias", async () => { //#given loadUserAgentsMock.mockImplementation(() => ({ '"plan"': { @@ -346,9 +345,9 @@ describe("resolveSubagentExecution", () => { const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") //#then - expect(result.agentToUse).toBe("") + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("plan") 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 () => {