fix(team-mode): preserve per-category model for kind:"category" members

The shared resolveCategoryExecution path treats `agents.sisyphus-junior.model`
(plugin config) as a higher-precedence override than the category default. That
ranking is correct for plain `task(category=...)` delegations - it lets users
pin sisyphus-junior to their preferred general model - but in team-mode it
collapses every kind:"category" member onto the same model.

Hyperplan was the visible victim: skeptic/validator/researcher/architect/creative
are routed through sisyphus-junior with five distinct categories
(unspecified-low / unspecified-high / deep / ultrabrain / artistry). With any
sisyphus-junior model configured, all five resolved to that single override
model instead of their category defaults, defeating the multi-model adversarial
debate.

Strip the override at the team-mode boundary in resolveMember rather than
changing resolveCategoryExecution itself, so:
- delegate-task callers keep the existing override semantics (3 regression
  tests at tools.test.ts:2832, 2958, 3020 stay green)
- per-category user overrides (`categories[X].model`) and explicit fallback
  chains continue to apply
- only the `kind:"category"` team-mode path opts out of the global override

Lock the contract with a regression test in resolve-member.test.ts.

Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode).
This commit is contained in:
YeonGyu-Kim
2026-04-30 15:58:31 +09:00
parent cc1780868a
commit d8f89b6965
2 changed files with 39 additions and 1 deletions
@@ -75,6 +75,36 @@ describe("resolveMember", () => {
expect(result.systemContent).toBe("resolved-system-content")
})
test("strips sisyphusJuniorModel before resolving category members so each declared category keeps its own model", async () => {
// given
const member = {
backendType: "in-process",
isActive: true,
kind: "category",
name: "architect",
category: "ultrabrain",
prompt: "design X",
} satisfies Member
const ctxWithJuniorOverride: ExecutorContext = {
...createExecutorContext(),
sisyphusJuniorModel: "anthropic/claude-sonnet-4-6",
}
resolveCategoryExecutionMock.mockResolvedValue({
agentToUse: "sisyphus-junior",
categoryModel: { providerID: "openai", modelID: "gpt-5.5", variant: "xhigh" },
categoryPromptAppend: "appendix",
maxPromptTokens: 256,
fallbackChain: [],
})
// when
await resolveMember(member, ctxWithJuniorOverride, "ultrabrain, deep")
// then
const [, executorCtxArg] = resolveCategoryExecutionMock.mock.calls[0]
expect(executorCtxArg.sisyphusJuniorModel).toBeUndefined()
})
test("routes subagent members through resolveSubagentExecution", async () => {
// given
const member = {
@@ -51,6 +51,14 @@ function resolveSystemContent(input: {
}) ?? ""
}
// Strip global `agents.sisyphus-junior.model` override at the team-mode boundary —
// `resolveCategoryExecution` ranks it above category defaults (correct for plain
// `task(category=…)`, wrong here) and would collapse every team member to the same model.
function withoutSisyphusJuniorOverride(ctx: ExecutorContext): ExecutorContext {
if (ctx.sisyphusJuniorModel === undefined) return ctx
return { ...ctx, sisyphusJuniorModel: undefined }
}
export async function resolveMember(
member: Member,
ctx: ExecutorContext,
@@ -65,7 +73,7 @@ export async function resolveMember(
category: member.category,
subagent_type: "sisyphus-junior",
},
ctx,
withoutSisyphusJuniorOverride(ctx),
undefined,
undefined,
)