Merge pull request #4117 from ririnto/fix/plan-subagent-hidden-registry
fix(delegate-task): restore hidden plan subagent delegation
This commit is contained in:
@@ -196,7 +196,7 @@ export function buildNonClaudePlannerSection(model: string): string {
|
||||
Multi-step task? **ALWAYS consult Plan Agent first.** Do NOT start implementation without a plan.
|
||||
|
||||
- Single-file fix or trivial change → proceed directly
|
||||
- Anything else (2+ steps, unclear scope, architecture) → \`task(subagent_type="prometheus", ...)\` FIRST
|
||||
- Anything else (2+ steps, unclear scope, architecture) → \`task(subagent_type="plan", ...)\` FIRST
|
||||
- Use \`task_id\` to resume the same Plan Agent - ask follow-up questions aggressively
|
||||
- If ANY part of the task is ambiguous, ask Plan Agent before guessing
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ Every implementation task follows this cycle. No exceptions.
|
||||
Follow \`<explore>\` protocol for tool usage and agent prompts.
|
||||
|
||||
2. PLAN - List files to modify, specific changes, dependencies, complexity estimate.
|
||||
Multi-step (2+) → consult Plan Agent via \`task(subagent_type="prometheus", ...)\`.
|
||||
Multi-step (2+) → consult Plan Agent via \`task(subagent_type="plan", ...)\`.
|
||||
Single-step → mental plan is sufficient.
|
||||
|
||||
<dependency_checks>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "./validator"
|
||||
|
||||
const PROMETHEUS_REJECTION_MESSAGE =
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use category: 'plan' instead."
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use delegate-task with subagent_type: 'plan' instead."
|
||||
|
||||
function createCategoryMember(name: string): Member {
|
||||
return {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe("team-mode types", () => {
|
||||
],
|
||||
[
|
||||
"prometheus",
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use category: 'plan' instead.",
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use delegate-task with subagent_type: 'plan' instead.",
|
||||
],
|
||||
] as const
|
||||
|
||||
@@ -286,7 +286,7 @@ describe("team-mode types", () => {
|
||||
"Agent 'momus' is read-only (plan reviewer). Cannot write to mailbox as team member. Use delegate-task for plan review instead.",
|
||||
)
|
||||
expect(AGENT_ELIGIBILITY_REGISTRY.prometheus.rejectionMessage).toBe(
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use category: 'plan' instead.",
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use delegate-task with subagent_type: 'plan' instead.",
|
||||
)
|
||||
expect(CategoryMemberSchema).toBeDefined()
|
||||
expect(SubagentMemberSchema).toBeDefined()
|
||||
|
||||
@@ -229,7 +229,7 @@ export const AGENT_ELIGIBILITY_REGISTRY: Readonly<Record<string, {
|
||||
prometheus: {
|
||||
verdict: "hard-reject",
|
||||
rejectionMessage:
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use category: 'plan' instead.",
|
||||
"Agent 'prometheus' is plan-mode-only; can only write to .omo/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use delegate-task with subagent_type: 'plan' instead.",
|
||||
},
|
||||
"sisyphus-junior": { verdict: "eligible" },
|
||||
} as const
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { loadProjectAgents, loadUserAgents } from "../../features/claude-code-agent-loader"
|
||||
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
|
||||
|
||||
@@ -16,7 +15,7 @@ export type AgentInfo = {
|
||||
}
|
||||
|
||||
export function sanitizeSubagentType(subagentType: string): string {
|
||||
return subagentType.trim().replace(/^[\\\/"']+|[\\\/"']+$/g, "").trim()
|
||||
return subagentType.trim().replace(/^[\\/"']+|[\\/"']+$/g, "").trim()
|
||||
}
|
||||
|
||||
export function mergeWithClaudeCodeAgents(
|
||||
@@ -69,10 +68,10 @@ export function isTaskCallableAgentMode(mode: AgentMode): boolean {
|
||||
return mode === "all" || mode === "subagent"
|
||||
}
|
||||
|
||||
function isDemotedPlanAgent(agent: AgentInfo): boolean {
|
||||
export function isDemotedPlanAgent(agent: AgentInfo): boolean {
|
||||
return agent.hidden === true
|
||||
&& agent.mode === "subagent"
|
||||
&& stripInvisibleAgentCharacters(agent.name).trim().toLowerCase() === "plan"
|
||||
&& stripAgentListSortPrefix(agent.name).trim().toLowerCase() === "plan"
|
||||
}
|
||||
|
||||
function isVisibleToTask(agent: AgentInfo): boolean {
|
||||
|
||||
@@ -1,30 +1,65 @@
|
||||
import type { DelegateTaskArgs } from "./types"
|
||||
import type { ExecutorContext } from "./executor-types"
|
||||
import type { DelegatedModelConfig } from "./types"
|
||||
import { isPlanFamily } from "./constants"
|
||||
import { isPlanAgent, isPlanFamily } from "./constants"
|
||||
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
||||
import { applyCategoryParams } from "./delegated-model-config"
|
||||
import { getAvailableModelsForDelegateTask } from "./available-models"
|
||||
import { resolveEffectiveFallbackEntry } from "./fallback-entry-resolution"
|
||||
import { applyFallbackEntrySettings } from "./fallback-entry-settings"
|
||||
import type { AgentInfo } from "./subagent-discovery"
|
||||
import {
|
||||
type AgentInfo,
|
||||
sanitizeSubagentType,
|
||||
mergeWithClaudeCodeAgents,
|
||||
findPrimaryAgentMatch,
|
||||
findCallableAgentMatch,
|
||||
sanitizeSubagentType,
|
||||
listCallableAgentNames,
|
||||
mergeWithClaudeCodeAgents,
|
||||
isDemotedPlanAgent,
|
||||
} from "./subagent-discovery"
|
||||
import { normalizeModelFormat } from "../../shared/model-format-normalizer"
|
||||
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||
import { normalizeFallbackModels, flattenToFallbackModelStrings } from "../../shared/model-resolver"
|
||||
import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models"
|
||||
import { getAgentConfigKey, stripAgentListSortPrefix } from "../../shared/agent-display-names"
|
||||
import { normalizeSDKResponse } from "../../shared"
|
||||
import { log } from "../../shared/logger"
|
||||
import { getAvailableModelsForDelegateTask } from "./available-models"
|
||||
import type { FallbackEntry } from "../../shared/model-requirements"
|
||||
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||
import { resolveModelForDelegateTask } from "./model-selection"
|
||||
import { fuzzyMatchModel } from "../../shared/model-availability"
|
||||
import { getAgentConfigKey, stripAgentListSortPrefix } from "../../shared/agent-display-names"
|
||||
import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models"
|
||||
import { normalizeSDKResponse } from "../../shared"
|
||||
import { normalizeModelFormat } from "../../shared/model-format-normalizer"
|
||||
import { flattenToFallbackModelStrings, normalizeFallbackModels } from "../../shared/model-resolver"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
const DEFAULT_PLAN_FALLBACK_AGENT = "plan"
|
||||
const RESERVED_HIDDEN_NATIVE_AGENTS = new Set(["build"])
|
||||
|
||||
function isReservedHiddenNativeAgent(agentName: string): boolean {
|
||||
return RESERVED_HIDDEN_NATIVE_AGENTS.has(getAgentConfigKey(agentName))
|
||||
}
|
||||
|
||||
function shouldUseHiddenPlanAgent(
|
||||
requestedAgent: string,
|
||||
serverPrimaryAgent: AgentInfo | undefined,
|
||||
serverMatchedAgent: AgentInfo | undefined,
|
||||
sisyphusAgentConfig: ExecutorContext["sisyphusAgentConfig"],
|
||||
hasDemotedPlan: boolean,
|
||||
): boolean {
|
||||
if (serverPrimaryAgent) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (hasDemotedPlan) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (serverMatchedAgent) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isPlanAgent(requestedAgent)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return sisyphusAgentConfig?.planner_enabled !== false
|
||||
&& sisyphusAgentConfig?.replace_plan !== false
|
||||
}
|
||||
|
||||
export interface ResolveSubagentExecutionOptions {
|
||||
allowSisyphusJuniorDirect?: boolean
|
||||
@@ -74,18 +109,36 @@ Create the work plan directly - that's your job as the planning agent.`,
|
||||
|
||||
let agentToUse = agentName
|
||||
let categoryModel: DelegatedModelConfig | undefined
|
||||
let fallbackChain: FallbackEntry[] | undefined = undefined
|
||||
let fallbackChain: FallbackEntry[] | undefined
|
||||
|
||||
try {
|
||||
const agentsResult = await client.app.agents()
|
||||
const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], {
|
||||
preferResponseOnMissingData: true,
|
||||
})
|
||||
const hasDemotedPlan = agents.some(isDemotedPlanAgent)
|
||||
const serverPrimaryAgent = findPrimaryAgentMatch(agents, agentToUse)
|
||||
const serverMatchedAgent = findCallableAgentMatch(agents, agentToUse)
|
||||
|
||||
const mergedAgents = mergeWithClaudeCodeAgents(agents, executorCtx.directory)
|
||||
const matchedPrimaryAgent = findPrimaryAgentMatch(mergedAgents, agentToUse)
|
||||
const useHiddenPlanFallback = shouldUseHiddenPlanAgent(
|
||||
agentToUse,
|
||||
serverPrimaryAgent,
|
||||
serverMatchedAgent,
|
||||
executorCtx.sisyphusAgentConfig,
|
||||
hasDemotedPlan,
|
||||
)
|
||||
|
||||
if (matchedPrimaryAgent && !options.allowPrimaryAgentDelegation) {
|
||||
if (isReservedHiddenNativeAgent(agentToUse) && !serverPrimaryAgent && !serverMatchedAgent) {
|
||||
return {
|
||||
agentToUse: "",
|
||||
categoryModel: undefined,
|
||||
error: `Unknown agent: "${agentToUse}". Available agents: ${listCallableAgentNames(agents)}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedPrimaryAgent && !options.allowPrimaryAgentDelegation && !useHiddenPlanFallback) {
|
||||
return {
|
||||
agentToUse: "",
|
||||
categoryModel: undefined,
|
||||
@@ -94,10 +147,17 @@ Create the work plan directly - that's your job as the planning agent.`,
|
||||
}
|
||||
|
||||
const usePrimary = options.allowPrimaryAgentDelegation && matchedPrimaryAgent !== undefined
|
||||
const matchedAgent = usePrimary
|
||||
let matchedAgent = usePrimary
|
||||
? matchedPrimaryAgent
|
||||
: findCallableAgentMatch(mergedAgents, agentToUse)
|
||||
|
||||
if (useHiddenPlanFallback) {
|
||||
matchedAgent = {
|
||||
name: DEFAULT_PLAN_FALLBACK_AGENT,
|
||||
mode: "subagent",
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedAgent) {
|
||||
return {
|
||||
agentToUse: "",
|
||||
@@ -153,7 +213,8 @@ Create the work plan directly - that's your job as the planning agent.`,
|
||||
categoryModel = applyCategoryParams(resolvedModel, agentCategoryConfig)
|
||||
}
|
||||
} else if (resolutionSkipped && (agentOverride?.model ?? agentCategoryModel)) {
|
||||
const normalized = normalizeModelFormat((agentOverride?.model ?? agentCategoryModel)!)
|
||||
const explicitModel = agentOverride?.model ?? agentCategoryModel
|
||||
const normalized = explicitModel ? normalizeModelFormat(explicitModel) : undefined
|
||||
if (normalized) {
|
||||
const variantToUse = agentOverride?.variant ?? agentCategoryConfig?.variant
|
||||
const resolvedModel = variantToUse ? { ...normalized, variant: variantToUse } : normalized
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||
import type { DelegateTaskArgs } from "../types"
|
||||
import type { ExecutorContext } from "../executor-types"
|
||||
|
||||
@@ -282,6 +282,270 @@ describe("resolveSubagentExecution", () => {
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
})
|
||||
|
||||
test("preserves hidden sort-prefixed plan agent model instead of using fallback", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { anthropic: ["claude-opus-4-7"] },
|
||||
connected: ["anthropic"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "1|plan", mode: "subagent", hidden: true, model: "anthropic/claude-opus-4-7" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("plan")
|
||||
expect(result.categoryModel).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-7" })
|
||||
})
|
||||
|
||||
test("allows OpenCode-hidden-list plan fallback when planner_enabled and replace_plan are true", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]), {
|
||||
sisyphusAgentConfig: {
|
||||
planner_enabled: true,
|
||||
replace_plan: true,
|
||||
},
|
||||
})
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("plan")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
})
|
||||
|
||||
test.each([
|
||||
{ loader: "user", aliasName: "plan" },
|
||||
{ loader: "user", aliasName: '"plan"' },
|
||||
{ loader: "project", aliasName: "plan" },
|
||||
{ loader: "project", aliasName: '"plan"' },
|
||||
])(
|
||||
"uses built-in hidden plan fallback when a $loader $aliasName alias exists",
|
||||
async ({ loader, aliasName }) => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.3-codex"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
|
||||
loadUserAgentsMock.mockImplementation(() => {
|
||||
if (loader === "user") {
|
||||
return {
|
||||
[aliasName]: {
|
||||
description: "Colliding plan alias from user agents",
|
||||
mode: "subagent",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
},
|
||||
} satisfies ClaudeCodeAgentRecord
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
loadProjectAgentsMock.mockImplementation(() => {
|
||||
if (loader === "project") {
|
||||
return {
|
||||
[aliasName]: {
|
||||
description: "Colliding plan alias from project agents",
|
||||
mode: "subagent",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
},
|
||||
} satisfies ClaudeCodeAgentRecord
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]), {
|
||||
sisyphusAgentConfig: {
|
||||
planner_enabled: true,
|
||||
replace_plan: true,
|
||||
},
|
||||
})
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("plan")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
},
|
||||
)
|
||||
|
||||
test.each([
|
||||
{ loader: "user", aliasName: "plan" },
|
||||
{ loader: "user", aliasName: '"plan"' },
|
||||
{ loader: "project", aliasName: "plan" },
|
||||
{ loader: "project", aliasName: '"plan"' },
|
||||
])(
|
||||
"uses built-in hidden plan fallback when a $loader primary $aliasName alias exists",
|
||||
async ({ loader, aliasName }) => {
|
||||
//#given
|
||||
loadUserAgentsMock.mockImplementation(() => {
|
||||
if (loader === "user") {
|
||||
return {
|
||||
[aliasName]: {
|
||||
description: "Colliding primary plan alias from user agents",
|
||||
mode: "primary",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
},
|
||||
} satisfies ClaudeCodeAgentRecord
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
loadProjectAgentsMock.mockImplementation(() => {
|
||||
if (loader === "project") {
|
||||
return {
|
||||
[aliasName]: {
|
||||
description: "Colliding primary plan alias from project agents",
|
||||
mode: "primary",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
},
|
||||
} satisfies ClaudeCodeAgentRecord
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]), {
|
||||
sisyphusAgentConfig: {
|
||||
planner_enabled: true,
|
||||
replace_plan: true,
|
||||
},
|
||||
})
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("plan")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
},
|
||||
)
|
||||
|
||||
test.each([
|
||||
{ loader: "user", aliasName: "build" },
|
||||
{ loader: "user", aliasName: '"build"' },
|
||||
{ loader: "user", aliasName: "1|build" },
|
||||
{ loader: "user", aliasName: "\u200Bbuild" },
|
||||
{ loader: "project", aliasName: "build" },
|
||||
{ loader: "project", aliasName: '"build"' },
|
||||
{ loader: "project", aliasName: "1|build" },
|
||||
{ loader: "project", aliasName: "\u200Bbuild" },
|
||||
])(
|
||||
"rejects omitted hidden build when a $loader $aliasName alias exists",
|
||||
async ({ loader, aliasName }) => {
|
||||
//#given
|
||||
loadUserAgentsMock.mockImplementation(() => {
|
||||
if (loader === "user") {
|
||||
return {
|
||||
[aliasName]: {
|
||||
description: "Colliding hidden build alias from user agents",
|
||||
mode: "subagent",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
},
|
||||
} satisfies ClaudeCodeAgentRecord
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
loadProjectAgentsMock.mockImplementation(() => {
|
||||
if (loader === "project") {
|
||||
return {
|
||||
[aliasName]: {
|
||||
description: "Colliding hidden build alias from project agents",
|
||||
mode: "subagent",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
},
|
||||
} satisfies ClaudeCodeAgentRecord
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const args = createBaseArgs({ subagent_type: "build" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ 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("preserves a visible server plan agent instead of using fallback", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.3-codex"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "plan", mode: "subagent", model: "openai/gpt-5.3-codex" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]), {
|
||||
sisyphusAgentConfig: {
|
||||
planner_enabled: true,
|
||||
replace_plan: true,
|
||||
},
|
||||
})
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("plan")
|
||||
expect(result.categoryModel).toEqual({ providerID: "openai", modelID: "gpt-5.3-codex" })
|
||||
})
|
||||
|
||||
test.each([
|
||||
[{ planner_enabled: false, replace_plan: true }],
|
||||
[{ planner_enabled: true, replace_plan: false }],
|
||||
])(
|
||||
"does not allow hidden plan fallback when planner config blocks replacement (%j)",
|
||||
async (sisyphusAgentConfig) => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]), {
|
||||
sisyphusAgentConfig,
|
||||
})
|
||||
|
||||
//#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 error hints except callable demoted plan", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "nonexistent" })
|
||||
|
||||
Reference in New Issue
Block a user