From ad85e43809c507f8b10f7b2ab232df346be6be3a Mon Sep 17 00:00:00 2001 From: trafgals Date: Fri, 3 Apr 2026 11:51:19 +1000 Subject: [PATCH 1/4] fix: include user/project agents in task(subagent_type) resolution Re-implement PR #2690 against current upstream/dev since original branch became incompatible with codebase refactoring. Changes: - Import loadUserAgents and loadProjectAgents from claude-code-agent-loader - Merge user/project agents with server agents in resolveSubagentExecution() - Server agents take precedence on name collisions - Primary-mode agents from user/project are filtered out Tests: 4 new test cases covering user agent resolution, project agent resolution, server precedence, and primary agent filtering. --- .../delegate-task/subagent-resolver.test.ts | 138 +++++++++++++++++- src/tools/delegate-task/subagent-resolver.ts | 55 ++++++- 2 files changed, 186 insertions(+), 7 deletions(-) diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/subagent-resolver.test.ts index 53cf0f0a4..28e83f761 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/subagent-resolver.test.ts @@ -1,11 +1,19 @@ declare const require: (name: string) => any -const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test") +const { describe, test, expect, beforeEach, afterEach, spyOn, mock, vi } = require("bun:test") import { resolveSubagentExecution } from "./subagent-resolver" import type { DelegateTaskArgs } from "./types" import type { ExecutorContext } from "./executor-types" import * as logger from "../../shared/logger" import * as connectedProvidersCache from "../../shared/connected-providers-cache" +const mockLoadUserAgents = vi.fn().mockReturnValue({}) +const mockLoadProjectAgents = vi.fn().mockReturnValue({}) + +vi.mock("../../features/claude-code-agent-loader", () => ({ + loadUserAgents: mockLoadUserAgents, + loadProjectAgents: mockLoadProjectAgents, +})) + function createBaseArgs(overrides?: Partial): DelegateTaskArgs { return { description: "Run review", @@ -507,4 +515,132 @@ describe("resolveSubagentExecution", () => { cacheSpy.mockRestore() connectedSpy.mockRestore() }) + + test("resolves user agent from loadUserAgents when calling task(subagent_type=...)", async () => { + //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { openai: ["gpt-5.4"] }, + connected: ["openai"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"]) + + mockLoadUserAgents.mockReturnValue({ + "my-user-agent": { + description: "A user agent", + mode: "subagent", + prompt: "Do something", + model: "openai/gpt-5.4", + }, + }) + mockLoadProjectAgents.mockReturnValue({}) + + const args = createBaseArgs({ subagent_type: "my-user-agent" }) + const executorCtx = createExecutorContext(async () => []) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("my-user-agent") + expect(result.categoryModel?.modelID).toBe("gpt-5.4") + + cacheSpy.mockRestore() + connectedSpy.mockRestore() + }) + + test("resolves project agent from loadProjectAgents when calling task(subagent_type=...)", async () => { + //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { anthropic: ["claude-sonnet-4"] }, + connected: ["anthropic"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"]) + + mockLoadUserAgents.mockReturnValue({}) + mockLoadProjectAgents.mockReturnValue({ + "my-project-agent": { + description: "A project agent", + mode: "subagent", + prompt: "Do project work", + model: "anthropic/claude-sonnet-4", + }, + }) + + const args = createBaseArgs({ subagent_type: "my-project-agent" }) + const executorCtx = createExecutorContext(async () => []) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("my-project-agent") + expect(result.categoryModel?.modelID).toBe("claude-sonnet-4") + + cacheSpy.mockRestore() + connectedSpy.mockRestore() + }) + + test("server agent takes precedence over user agent with same name", async () => { + //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { openai: ["gpt-5.4"] }, + connected: ["openai"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"]) + + mockLoadUserAgents.mockReturnValue({ + "explore": { + description: "User explore agent", + mode: "subagent", + prompt: "User prompt", + model: "openai/gpt-3.5", + }, + }) + mockLoadProjectAgents.mockReturnValue({}) + + // Server has "explore" agent + const args = createBaseArgs({ subagent_type: "explore" }) + const executorCtx = createExecutorContext(async () => ([ + { name: "explore", mode: "subagent", model: "openai/gpt-5.4" }, + ])) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("explore") + // Should use server's model, not user's + expect(result.categoryModel?.modelID).toBe("gpt-5.4") + + cacheSpy.mockRestore() + connectedSpy.mockRestore() + }) + + test("filters out primary agents from user/project when resolving", async () => { + //#given + mockLoadUserAgents.mockReturnValue({ + "my-primary-agent": { + description: "A primary agent", + mode: "primary", + prompt: "I am primary", + }, + }) + mockLoadProjectAgents.mockReturnValue({}) + + const args = createBaseArgs({ subagent_type: "my-primary-agent" }) + const executorCtx = createExecutorContext(async () => []) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toContain("Unknown agent") + expect(result.agentToUse).toBe("") + }) }) diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index 12e97f52a..db2f8b75d 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -14,6 +14,7 @@ import { getAvailableModelsForDelegateTask } from "./available-models" import type { FallbackEntry } from "../../shared/model-requirements" import { resolveModelForDelegateTask } from "./model-selection" import { fuzzyMatchModel } from "../../shared/model-availability" +import { loadUserAgents, loadProjectAgents } from "../../features/claude-code-agent-loader" export async function resolveSubagentExecution( args: DelegateTaskArgs, @@ -53,18 +54,60 @@ Create the work plan directly - that's your job as the planning agent.`, let categoryModel: DelegatedModelConfig | undefined let fallbackChain: FallbackEntry[] | undefined = undefined + type AgentInfo = { + name: string + mode?: "subagent" | "primary" | "all" + model?: string | { providerID: string; modelID: string } + } + try { const agentsResult = await client.app.agents() - type AgentInfo = { - name: string - mode?: "subagent" | "primary" | "all" - model?: string | { providerID: string; modelID: string } - } const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], { preferResponseOnMissingData: true, }) - const callableAgents = agents.filter((a) => a.mode !== "primary") + // Load user and project agents + const userAgentsRecord = loadUserAgents() + const projectAgentsRecord = loadProjectAgents(executorCtx.directory) + + // Convert user/project agent configs to AgentInfo format + const userAgentsList: AgentInfo[] = Object.entries(userAgentsRecord).map(([name, config]) => ({ + name, + mode: config.mode as "subagent" | "primary" | "all", + model: config.model, + })) + + const projectAgentsList: AgentInfo[] = Object.entries(projectAgentsRecord).map(([name, config]) => ({ + name, + mode: config.mode as "subagent" | "primary" | "all", + model: config.model, + })) + + // Merge user and project agents into the server's agent list + // Server agents take precedence; user/project agents fill in gaps + const mergedAgentMap = new Map() + + // First add server agents (they take precedence) + for (const agent of agents) { + mergedAgentMap.set(agent.name.toLowerCase(), agent) + } + + // Then add user agents (server wins on collision) + for (const agent of userAgentsList) { + if (!mergedAgentMap.has(agent.name.toLowerCase())) { + mergedAgentMap.set(agent.name.toLowerCase(), agent) + } + } + + // Then add project agents (server wins on collision) + for (const agent of projectAgentsList) { + if (!mergedAgentMap.has(agent.name.toLowerCase())) { + mergedAgentMap.set(agent.name.toLowerCase(), agent) + } + } + + const mergedAgents = Array.from(mergedAgentMap.values()) + const callableAgents = mergedAgents.filter((a) => a.mode !== "primary") const resolvedDisplayName = getAgentDisplayName(agentToUse) const matchedAgent = callableAgents.find( From 2266f28f75b696eb4857b3dc9d4b08696f76f03e Mon Sep 17 00:00:00 2001 From: trafgals Date: Fri, 3 Apr 2026 11:51:20 +1000 Subject: [PATCH 2/4] fix: correct merge order so project agents override user agents Address PR review comment: project agents should take precedence over user agents when both define the same agent name. --- bun.lock | 44 +++++++++---------- .../delegate-task/subagent-resolver.test.ts | 30 +++++++++++++ src/tools/delegate-task/subagent-resolver.ts | 4 +- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index 37953d5e9..4e96d0f2c 100644 --- a/bun.lock +++ b/bun.lock @@ -29,17 +29,17 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.11.0", - "oh-my-opencode-darwin-x64": "3.11.0", - "oh-my-opencode-darwin-x64-baseline": "3.11.0", - "oh-my-opencode-linux-arm64": "3.11.0", - "oh-my-opencode-linux-arm64-musl": "3.11.0", - "oh-my-opencode-linux-x64": "3.11.0", - "oh-my-opencode-linux-x64-baseline": "3.11.0", - "oh-my-opencode-linux-x64-musl": "3.11.0", - "oh-my-opencode-linux-x64-musl-baseline": "3.11.0", - "oh-my-opencode-windows-x64": "3.11.0", - "oh-my-opencode-windows-x64-baseline": "3.11.0", + "oh-my-opencode-darwin-arm64": "3.14.0", + "oh-my-opencode-darwin-x64": "3.14.0", + "oh-my-opencode-darwin-x64-baseline": "3.14.0", + "oh-my-opencode-linux-arm64": "3.14.0", + "oh-my-opencode-linux-arm64-musl": "3.14.0", + "oh-my-opencode-linux-x64": "3.14.0", + "oh-my-opencode-linux-x64-baseline": "3.14.0", + "oh-my-opencode-linux-x64-musl": "3.14.0", + "oh-my-opencode-linux-x64-musl-baseline": "3.14.0", + "oh-my-opencode-windows-x64": "3.14.0", + "oh-my-opencode-windows-x64-baseline": "3.14.0", }, }, }, @@ -238,27 +238,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.11.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-TLMCq1HXU1BOp3KWdcITQqT3TQcycAxvdYELMzY/17HUVHjvJiaLjyrbmw0VlgBjoRZOlmsedK+o59y7WRM40Q=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.14.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-i32X3vSfHc1aD4VBD2FJoyGC+uLN3BVmfR0kKO4miA0pZfpMGrpD2NW3Ts6qO25E9czCOWfbbiYgbmfdBm2tzQ=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-szKfyAYbI3Mp6rqxHxcHhAE8noxIzBbpfvKX0acyMB/KRqUCtgTe13aic5tz/W/Agp9NU1PVasyqjJjAtE73JA=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.14.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-4lPI2/vmpoKpTs/59YyMviMzagsWB/uf8rmMIwINxHADziVyMnJSrR1PQqu24vLL2VUoZMcU2uGPFSXFeKkDug=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-QZ+2LCcXK6NPopYSxFCHrYAqLccN+jMQ0YrQI+QBlsajLSsnSqfv6W3Vaxv95iLWhGey3v2oGu5OUgdW9fjy9w=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.14.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-yag/GPdVaywHQ7wZ5EPIb+rCDv2WBYe0lo/XfxAyGJf24XLIc2tS0cD4iZVtHdJ7QtIu5HGiO2uxKAxnZp1IOg=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-NZMbNG+kJ0FTS4u5xhuBUjJ2K2Tds8sETbdq1VPT52rd+mIbVVSbugfppagEh9wbNqXqJY1HwQ/+4Q+NoGGXhQ=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.14.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oflhCC+TFbGqy0A3/bxskQiWLaZjmtnS2arwBSGGm9JeAaJabVwB7JKH+F8o6Dr9IWUhZSuQEbkCVXIjTAwHVw=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-f0GO63uAwzBisotiMneA7Pi2xPXUxvdX5QRC6z4X2xoB8F7/jT+2+dY8J03eM+YJVAwQWR/74hm5HFSenqMeIA=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.14.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-THIZFvMIDY/KM/zYYwmmkfsWoLRNOd/NTHYBtt90Rac9mjoxLp9XAbwNdqRGeaWJhl3Qq525k6OkJTwYTDsrSg=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OzIgo26t1EbooHwzmli+4aemO6YqXEhJTBth8L688K1CI/xF567G3+uJemZ9U7NI+miHJRoKHcidNnaAi7bgGQ=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.14.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KFGZNaYzMt2nFARycHHko6ciMa6EJtg9MTTGcVDkPvuSADO7nyMBH2txHIcyJDchkCraM35MR8h7yZtdSRJNuQ=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ac7TfBli+gaHVu4aBtP2ADWzetrFZOs+h1K39KsR6MOhDZBl+B6B1S47U+BXGWtUKIRYm4uUo578XdnmsDanoA=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.14.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-wSD71xhwh8brkWtMikJr8wqhcoRn+AemGlSSFQjLZz9Xmn5waXSZlfwx1N4toZczPEEpBF6GL1eZH/Kdnu4cdg=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OvOsPNuvZQug4tGjbcpbvh67tud1K84A3Qskt9S7BHBIvMH129iV/2GGyr6aca8gwvd5T+X05H/s5mnPG6jkBQ=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.14.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-A5vT8QuUMmgreRXXlPyx/pPZOetW4Zwl/oGEWBBM2m63j2cisp3C4FjeWiqE+UACEYVLitybnEWwEswzC678Xw=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-fSsyVAFMoOljD+zqRO6lG3f9ka1YRLMp6rNSsPWkLEKKIyEdw1J0GcmA/48VI1NgtnEgKqS3Ft87tees1woyBw=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.14.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-jPBqAA1iOQS+I1jJchQO2X/ItMJEuzkw/4yRYH9Yq1r6a9y0akApWdujsgMk5+vNMivv8jlMBgWKSPOoX3afwA=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-k9F3/9r3pFnUVJW36+zF06znUdUzcnJp+BdvDcaJrcuuM516ECwCH0yY5WbDTFFydFBQBkPBJX9DwU8dmc4kHA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.14.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-zyBQUxPvdDxItjq+5MzMrBwrVIVW6Spssyj6CQ3U50WbFaKIbyRGqe81JBQ1h0Gb4X43fLxiUBc0f1sWD0cv/Q=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mRRcCHC43TLUuIkDs0ASAUGo3DpMIkSeIPDdtBrh1eJZyVulJRGBoniIk/+Y+RJwtsUoC+lUX/auQelzJsMpbQ=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.14.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-BoqjPPCKXX+ehbwUqpZB9f/DL38ijbk+cuJtnhTkqm33op3cCkqK1ethmPVt7t7vZXJOYoFn2/ykd2NEQj7x0A=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/subagent-resolver.test.ts index 28e83f761..fe769df5f 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/subagent-resolver.test.ts @@ -622,6 +622,36 @@ describe("resolveSubagentExecution", () => { connectedSpy.mockRestore() }) + test("project agent takes precedence over user agent with same name", async () => { + //#given + mockLoadUserAgents.mockReturnValue({ + "my-custom-agent": { + description: "User agent", + mode: "subagent", + prompt: "User prompt", + model: "minimaxi/claude-3-haiku", + }, + }) + mockLoadProjectAgents.mockReturnValue({ + "my-custom-agent": { + description: "Project agent", + mode: "subagent", + prompt: "Project prompt", + model: "minimaxi/MiniMax-M2.7-highspeed", + }, + }) + + const args = createBaseArgs({ subagent_type: "my-custom-agent" }) + const executorCtx = createExecutorContext(async () => []) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("my-custom-agent") + }) + test("filters out primary agents from user/project when resolving", async () => { //#given mockLoadUserAgents.mockReturnValue({ diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index db2f8b75d..21d04f470 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -92,14 +92,14 @@ Create the work plan directly - that's your job as the planning agent.`, mergedAgentMap.set(agent.name.toLowerCase(), agent) } - // Then add user agents (server wins on collision) + // Then add user agents (server/project agents win on collision) for (const agent of userAgentsList) { if (!mergedAgentMap.has(agent.name.toLowerCase())) { mergedAgentMap.set(agent.name.toLowerCase(), agent) } } - // Then add project agents (server wins on collision) + // Then add project agents (overrides user agents, server wins on collision) for (const agent of projectAgentsList) { if (!mergedAgentMap.has(agent.name.toLowerCase())) { mergedAgentMap.set(agent.name.toLowerCase(), agent) From 64408823ae639fea56de5aaec5fef7b16407c53f Mon Sep 17 00:00:00 2001 From: trafgals Date: Fri, 3 Apr 2026 11:51:22 +1000 Subject: [PATCH 3/4] fix: actually swap merge order so project is added before user Previous fix only changed comments, not logic. Now project agents are added to the map before user agents, so project takes precedence. --- src/tools/delegate-task/subagent-resolver.test.ts | 11 +++++++++++ src/tools/delegate-task/subagent-resolver.ts | 10 +++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/subagent-resolver.test.ts index fe769df5f..013a256ed 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/subagent-resolver.test.ts @@ -624,6 +624,13 @@ describe("resolveSubagentExecution", () => { test("project agent takes precedence over user agent with same name", async () => { //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { minimaxi: ["MiniMax-M2.7-highspeed", "claude-3-haiku"] }, + connected: ["minimaxi"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["minimaxi"]) + mockLoadUserAgents.mockReturnValue({ "my-custom-agent": { description: "User agent", @@ -650,6 +657,10 @@ describe("resolveSubagentExecution", () => { //#then expect(result.error).toBeUndefined() expect(result.agentToUse).toBe("my-custom-agent") + expect(result.categoryModel?.modelID).toBe("MiniMax-M2.7-highspeed") + + cacheSpy.mockRestore() + connectedSpy.mockRestore() }) test("filters out primary agents from user/project when resolving", async () => { diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index 21d04f470..b8d729183 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -84,7 +84,7 @@ Create the work plan directly - that's your job as the planning agent.`, })) // Merge user and project agents into the server's agent list - // Server agents take precedence; user/project agents fill in gaps + // Server agents take precedence; project agents override user agents const mergedAgentMap = new Map() // First add server agents (they take precedence) @@ -92,15 +92,15 @@ Create the work plan directly - that's your job as the planning agent.`, mergedAgentMap.set(agent.name.toLowerCase(), agent) } - // Then add user agents (server/project agents win on collision) - for (const agent of userAgentsList) { + // Then add project agents (overrides user agents, server wins on collision) + for (const agent of projectAgentsList) { if (!mergedAgentMap.has(agent.name.toLowerCase())) { mergedAgentMap.set(agent.name.toLowerCase(), agent) } } - // Then add project agents (overrides user agents, server wins on collision) - for (const agent of projectAgentsList) { + // Then add user agents (only if not already added by server or project) + for (const agent of userAgentsList) { if (!mergedAgentMap.has(agent.name.toLowerCase())) { mergedAgentMap.set(agent.name.toLowerCase(), agent) } From 5441c848bad5904549c64fb3f1c402a5d71244be Mon Sep 17 00:00:00 2001 From: trafgals Date: Sat, 4 Apr 2026 07:23:12 +1000 Subject: [PATCH 4/4] refactor: use spyOn instead of vi.mock for agent loader mocks Use spyOn which is more idiomatic for bun:test, as suggested in PR review. --- src/tools/delegate-task/subagent-resolver.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/subagent-resolver.test.ts index 013a256ed..825eeccf2 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/subagent-resolver.test.ts @@ -5,14 +5,7 @@ import type { DelegateTaskArgs } from "./types" import type { ExecutorContext } from "./executor-types" import * as logger from "../../shared/logger" import * as connectedProvidersCache from "../../shared/connected-providers-cache" - -const mockLoadUserAgents = vi.fn().mockReturnValue({}) -const mockLoadProjectAgents = vi.fn().mockReturnValue({}) - -vi.mock("../../features/claude-code-agent-loader", () => ({ - loadUserAgents: mockLoadUserAgents, - loadProjectAgents: mockLoadProjectAgents, -})) +import * as agentLoader from "../../features/claude-code-agent-loader" function createBaseArgs(overrides?: Partial): DelegateTaskArgs { return { @@ -45,14 +38,20 @@ function createExecutorContext( describe("resolveSubagentExecution", () => { let logSpy: ReturnType | undefined + let mockLoadUserAgents: ReturnType + let mockLoadProjectAgents: ReturnType beforeEach(() => { mock.restore() logSpy = spyOn(logger, "log").mockImplementation(() => {}) + mockLoadUserAgents = spyOn(agentLoader, "loadUserAgents").mockReturnValue({}) + mockLoadProjectAgents = spyOn(agentLoader, "loadProjectAgents").mockReturnValue({}) }) afterEach(() => { logSpy?.mockRestore() + mockLoadUserAgents?.mockRestore() + mockLoadProjectAgents?.mockRestore() }) test("returns delegation error when agent discovery fails instead of silently proceeding", async () => {