diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index d33f37184..e88f2b4e4 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -600,6 +600,187 @@ describe("Prometheus direct override priority over category", () => { }) }) +describe("Plan agent model inheritance from prometheus", () => { + test("plan agent inherits all model-related settings from resolved prometheus config", async () => { + //#given - prometheus resolves to claude-opus-4-6 with model settings + spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ + model: "anthropic/claude-opus-4-6", + provenance: "provider-fallback", + variant: "max", + }) + const pluginConfig: OhMyOpenCodeConfig = { + sisyphus_agent: { + planner_enabled: true, + replace_plan: true, + }, + } + const config: Record = { + model: "anthropic/claude-opus-4-6", + agent: { + plan: { + name: "plan", + mode: "primary", + prompt: "original plan prompt", + }, + }, + } + const handler = createConfigHandler({ + ctx: { directory: "/tmp" }, + pluginConfig, + modelCacheState: { + anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), + }, + }) + + //#when + await handler(config) + + //#then - plan inherits model and variant from prometheus, but NOT prompt + const agents = config.agent as Record + expect(agents.plan).toBeDefined() + expect(agents.plan.mode).toBe("subagent") + expect(agents.plan.model).toBe("anthropic/claude-opus-4-6") + expect(agents.plan.variant).toBe("max") + expect(agents.plan.prompt).toBeUndefined() + }) + + test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => { + //#given - prometheus configured with category that has temperature and reasoningEffort + spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ + model: "openai/gpt-5.2", + provenance: "override", + variant: "high", + }) + const pluginConfig: OhMyOpenCodeConfig = { + sisyphus_agent: { + planner_enabled: true, + replace_plan: true, + }, + agents: { + prometheus: { + model: "openai/gpt-5.2", + variant: "high", + temperature: 0.3, + top_p: 0.9, + maxTokens: 16000, + reasoningEffort: "high", + textVerbosity: "medium", + thinking: { type: "enabled", budgetTokens: 8000 }, + }, + }, + } + const config: Record = { + model: "anthropic/claude-opus-4-6", + agent: {}, + } + const handler = createConfigHandler({ + ctx: { directory: "/tmp" }, + pluginConfig, + modelCacheState: { + anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), + }, + }) + + //#when + await handler(config) + + //#then - plan inherits ALL model-related settings from resolved prometheus + const agents = config.agent as Record> + expect(agents.plan).toBeDefined() + expect(agents.plan.mode).toBe("subagent") + expect(agents.plan.model).toBe("openai/gpt-5.2") + expect(agents.plan.variant).toBe("high") + expect(agents.plan.temperature).toBe(0.3) + expect(agents.plan.top_p).toBe(0.9) + expect(agents.plan.maxTokens).toBe(16000) + expect(agents.plan.reasoningEffort).toBe("high") + expect(agents.plan.textVerbosity).toBe("medium") + expect(agents.plan.thinking).toEqual({ type: "enabled", budgetTokens: 8000 }) + }) + + test("plan agent user override takes priority over prometheus inherited settings", async () => { + //#given - prometheus resolves to opus, but user has plan override for gpt-5.2 + spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ + model: "anthropic/claude-opus-4-6", + provenance: "provider-fallback", + variant: "max", + }) + const pluginConfig: OhMyOpenCodeConfig = { + sisyphus_agent: { + planner_enabled: true, + replace_plan: true, + }, + agents: { + plan: { + model: "openai/gpt-5.2", + variant: "high", + temperature: 0.5, + }, + }, + } + const config: Record = { + model: "anthropic/claude-opus-4-6", + agent: {}, + } + const handler = createConfigHandler({ + ctx: { directory: "/tmp" }, + pluginConfig, + modelCacheState: { + anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), + }, + }) + + //#when + await handler(config) + + //#then - plan uses its own override, not prometheus settings + const agents = config.agent as Record> + expect(agents.plan.model).toBe("openai/gpt-5.2") + expect(agents.plan.variant).toBe("high") + expect(agents.plan.temperature).toBe(0.5) + }) + + test("plan agent does NOT inherit prompt, description, or color from prometheus", async () => { + //#given + spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ + model: "anthropic/claude-opus-4-6", + provenance: "provider-fallback", + variant: "max", + }) + const pluginConfig: OhMyOpenCodeConfig = { + sisyphus_agent: { + planner_enabled: true, + replace_plan: true, + }, + } + const config: Record = { + model: "anthropic/claude-opus-4-6", + agent: {}, + } + const handler = createConfigHandler({ + ctx: { directory: "/tmp" }, + pluginConfig, + modelCacheState: { + anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), + }, + }) + + //#when + await handler(config) + + //#then - plan has model settings but NOT prompt/description/color + const agents = config.agent as Record> + expect(agents.plan.model).toBe("anthropic/claude-opus-4-6") + expect(agents.plan.prompt).toBeUndefined() + expect(agents.plan.description).toBeUndefined() + expect(agents.plan.color).toBeUndefined() + }) +}) + describe("Deadlock prevention - fetchAvailableModels must not receive client", () => { test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => { // given - This test ensures we don't regress on issue #1301 diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index 41adbaf20..ea7c2856c 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -32,6 +32,7 @@ import { AGENT_NAME_MAP } from "../shared/migration"; import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements"; import { PROMETHEUS_SYSTEM_PROMPT, PROMETHEUS_PERMISSION } from "../agents/prometheus"; import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants"; +import { buildPlanDemoteConfig } from "./plan-model-inheritance"; import type { ModelCacheState } from "../plugin-state"; import type { CategoryConfig } from "../config/schema"; @@ -385,8 +386,10 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { : {}; const planDemoteConfig = shouldDemotePlan - ? { mode: "subagent" as const - } + ? buildPlanDemoteConfig( + agentConfig["prometheus"] as Record | undefined, + pluginConfig.agents?.plan as Record | undefined, + ) : undefined; config.agent = { diff --git a/src/plugin-handlers/plan-model-inheritance.test.ts b/src/plugin-handlers/plan-model-inheritance.test.ts new file mode 100644 index 000000000..3b68f0a16 --- /dev/null +++ b/src/plugin-handlers/plan-model-inheritance.test.ts @@ -0,0 +1,118 @@ +import { describe, test, expect } from "bun:test" +import { buildPlanDemoteConfig } from "./plan-model-inheritance" + +describe("buildPlanDemoteConfig", () => { + test("returns only mode when prometheus and plan override are both undefined", () => { + //#given + const prometheusConfig = undefined + const planOverride = undefined + + //#when + const result = buildPlanDemoteConfig(prometheusConfig, planOverride) + + //#then + expect(result).toEqual({ mode: "subagent" }) + }) + + test("extracts all model settings from prometheus config", () => { + //#given + const prometheusConfig = { + name: "prometheus", + model: "anthropic/claude-opus-4-6", + variant: "max", + mode: "all", + prompt: "You are Prometheus...", + permission: { edit: "allow" }, + description: "Plan agent (Prometheus)", + color: "#FF5722", + temperature: 0.1, + top_p: 0.95, + maxTokens: 32000, + thinking: { type: "enabled", budgetTokens: 10000 }, + reasoningEffort: "high", + textVerbosity: "medium", + providerOptions: { key: "value" }, + } + + //#when + const result = buildPlanDemoteConfig(prometheusConfig, undefined) + + //#then - picks model settings, NOT prompt/permission/description/color/name/mode + expect(result.mode).toBe("subagent") + expect(result.model).toBe("anthropic/claude-opus-4-6") + expect(result.variant).toBe("max") + expect(result.temperature).toBe(0.1) + expect(result.top_p).toBe(0.95) + expect(result.maxTokens).toBe(32000) + expect(result.thinking).toEqual({ type: "enabled", budgetTokens: 10000 }) + expect(result.reasoningEffort).toBe("high") + expect(result.textVerbosity).toBe("medium") + expect(result.providerOptions).toEqual({ key: "value" }) + expect(result.prompt).toBeUndefined() + expect(result.permission).toBeUndefined() + expect(result.description).toBeUndefined() + expect(result.color).toBeUndefined() + expect(result.name).toBeUndefined() + }) + + test("plan override takes priority over prometheus for all model settings", () => { + //#given + const prometheusConfig = { + model: "anthropic/claude-opus-4-6", + variant: "max", + temperature: 0.1, + reasoningEffort: "high", + } + const planOverride = { + model: "openai/gpt-5.2", + variant: "high", + temperature: 0.5, + reasoningEffort: "low", + } + + //#when + const result = buildPlanDemoteConfig(prometheusConfig, planOverride) + + //#then + expect(result.model).toBe("openai/gpt-5.2") + expect(result.variant).toBe("high") + expect(result.temperature).toBe(0.5) + expect(result.reasoningEffort).toBe("low") + }) + + test("falls back to prometheus when plan override has partial settings", () => { + //#given + const prometheusConfig = { + model: "anthropic/claude-opus-4-6", + variant: "max", + temperature: 0.1, + reasoningEffort: "high", + } + const planOverride = { + model: "openai/gpt-5.2", + } + + //#when + const result = buildPlanDemoteConfig(prometheusConfig, planOverride) + + //#then - plan model wins, rest inherits from prometheus + expect(result.model).toBe("openai/gpt-5.2") + expect(result.variant).toBe("max") + expect(result.temperature).toBe(0.1) + expect(result.reasoningEffort).toBe("high") + }) + + test("skips undefined values from both sources", () => { + //#given + const prometheusConfig = { + model: "anthropic/claude-opus-4-6", + } + + //#when + const result = buildPlanDemoteConfig(prometheusConfig, undefined) + + //#then + expect(result).toEqual({ mode: "subagent", model: "anthropic/claude-opus-4-6" }) + expect(Object.keys(result)).toEqual(["mode", "model"]) + }) +}) diff --git a/src/plugin-handlers/plan-model-inheritance.ts b/src/plugin-handlers/plan-model-inheritance.ts new file mode 100644 index 000000000..bb32483c5 --- /dev/null +++ b/src/plugin-handlers/plan-model-inheritance.ts @@ -0,0 +1,27 @@ +const MODEL_SETTINGS_KEYS = [ + "model", + "variant", + "temperature", + "top_p", + "maxTokens", + "thinking", + "reasoningEffort", + "textVerbosity", + "providerOptions", +] as const + +export function buildPlanDemoteConfig( + prometheusConfig: Record | undefined, + planOverride: Record | undefined, +): Record { + const modelSettings: Record = {} + + for (const key of MODEL_SETTINGS_KEYS) { + const value = planOverride?.[key] ?? prometheusConfig?.[key] + if (value !== undefined) { + modelSettings[key] = value + } + } + + return { mode: "subagent" as const, ...modelSettings } +} diff --git a/src/tools/delegate-task/constants.ts b/src/tools/delegate-task/constants.ts index 2bc8f95fd..99744f8ce 100644 --- a/src/tools/delegate-task/constants.ts +++ b/src/tools/delegate-task/constants.ts @@ -538,7 +538,7 @@ export function buildPlanAgentSystemPrepend( * List of agent names that should be treated as plan agents. * Case-insensitive matching is used. */ -export const PLAN_AGENT_NAMES = ["plan", "prometheus", "planner"] +export const PLAN_AGENT_NAMES = ["plan", "planner"] /** * Check if the given agent name is a plan agent. diff --git a/src/tools/delegate-task/executor.ts b/src/tools/delegate-task/executor.ts index 2a3be2b2d..1f43491b0 100644 --- a/src/tools/delegate-task/executor.ts +++ b/src/tools/delegate-task/executor.ts @@ -12,7 +12,7 @@ import { resolveMultipleSkillsAsync } from "../../features/opencode-skill-loader import { discoverSkills } from "../../features/opencode-skill-loader" import { getTaskToastManager } from "../../features/task-toast-manager" import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state" -import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry } from "../../shared" +import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "../../shared" import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability" import { readConnectedProvidersCache } from "../../shared/connected-providers-cache" import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements" @@ -211,7 +211,7 @@ export async function executeSyncContinuation( : undefined } - await (client.session as any).promptAsync({ + await promptSyncWithModelSuggestionRetry(client, { path: { id: args.session_id! }, body: { ...(resumeAgent !== undefined ? { agent: resumeAgent } : {}), @@ -233,30 +233,6 @@ export async function executeSyncContinuation( return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}` } - const timing = getTimingConfig() - const pollStart = Date.now() - let lastMsgCount = 0 - let stablePolls = 0 - - while (Date.now() - pollStart < 60000) { - await new Promise(resolve => setTimeout(resolve, timing.POLL_INTERVAL_MS)) - - const elapsed = Date.now() - pollStart - if (elapsed < timing.SESSION_CONTINUATION_STABILITY_MS) continue - - const messagesCheck = await client.session.messages({ path: { id: args.session_id! } }) - const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array - const currentMsgCount = msgs.length - - if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) { - stablePolls++ - if (stablePolls >= timing.STABILITY_POLLS_REQUIRED) break - } else { - stablePolls = 0 - lastMsgCount = currentMsgCount - } - } - const messagesResult = await client.session.messages({ path: { id: args.session_id! }, }) @@ -621,7 +597,7 @@ export async function executeSyncTask( try { const allowTask = isPlanAgent(agentToUse) - await promptWithModelSuggestionRetry(client, { + await promptSyncWithModelSuggestionRetry(client, { path: { id: sessionID }, body: { agent: agentToUse, @@ -659,70 +635,6 @@ export async function executeSyncTask( }) } - const syncTiming = getTimingConfig() - const pollStart = Date.now() - let lastMsgCount = 0 - let stablePolls = 0 - let pollCount = 0 - - log("[task] Starting poll loop", { sessionID, agentToUse }) - - while (Date.now() - pollStart < syncTiming.MAX_POLL_TIME_MS) { - if (ctx.abort?.aborted) { - log("[task] Aborted by user", { sessionID }) - if (toastManager && taskId) toastManager.removeTask(taskId) - return `Task aborted.\n\nSession ID: ${sessionID}` - } - - await new Promise(resolve => setTimeout(resolve, syncTiming.POLL_INTERVAL_MS)) - pollCount++ - - const statusResult = await client.session.status() - const allStatuses = (statusResult.data ?? {}) as Record - const sessionStatus = allStatuses[sessionID] - - if (pollCount % 10 === 0) { - log("[task] Poll status", { - sessionID, - pollCount, - elapsed: Math.floor((Date.now() - pollStart) / 1000) + "s", - sessionStatus: sessionStatus?.type ?? "not_in_status", - stablePolls, - lastMsgCount, - }) - } - - if (sessionStatus && sessionStatus.type !== "idle") { - stablePolls = 0 - lastMsgCount = 0 - continue - } - - const elapsed = Date.now() - pollStart - if (elapsed < syncTiming.MIN_STABILITY_TIME_MS) { - continue - } - - const messagesCheck = await client.session.messages({ path: { id: sessionID } }) - const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array - const currentMsgCount = msgs.length - - if (currentMsgCount === lastMsgCount) { - stablePolls++ - if (stablePolls >= syncTiming.STABILITY_POLLS_REQUIRED) { - log("[task] Poll complete - messages stable", { sessionID, pollCount, currentMsgCount }) - break - } - } else { - stablePolls = 0 - lastMsgCount = currentMsgCount - } - } - - if (Date.now() - pollStart >= syncTiming.MAX_POLL_TIME_MS) { - log("[task] Poll timeout reached", { sessionID, pollCount, lastMsgCount, stablePolls }) - } - const messagesResult = await client.session.messages({ path: { id: sessionID }, }) @@ -963,7 +875,7 @@ Sisyphus-Junior is spawned automatically when you specify a category. Pick the a return { agentToUse: "", categoryModel: undefined, - error: `You are prometheus. You cannot delegate to prometheus via task. + error: `You are the plan agent. You cannot delegate to plan via task. Create the work plan directly - that's your job as the planning agent.`, } diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index 63a42297f..773022439 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -135,12 +135,12 @@ describe("sisyphus-task", () => { expect(result).toBe(true) }) - test("returns true for 'prometheus'", () => { - // given / #when + test("returns false for 'prometheus' (decoupled from plan)", () => { + //#given / #when const result = isPlanAgent("prometheus") - // then - expect(result).toBe(true) + //#then - prometheus is NOT a plan agent + expect(result).toBe(false) }) test("returns true for 'planner'", () => { @@ -159,12 +159,12 @@ describe("sisyphus-task", () => { expect(result).toBe(true) }) - test("returns true for case-insensitive match 'Prometheus'", () => { - // given / #when + test("returns false for case-insensitive match 'Prometheus' (decoupled from plan)", () => { + //#given / #when const result = isPlanAgent("Prometheus") - // then - expect(result).toBe(true) + //#then - Prometheus is NOT a plan agent + expect(result).toBe(false) }) test("returns false for 'oracle'", () => { @@ -199,11 +199,11 @@ describe("sisyphus-task", () => { expect(result).toBe(false) }) - test("PLAN_AGENT_NAMES contains expected values", () => { - // given / #when / #then + test("PLAN_AGENT_NAMES contains only plan and planner (not prometheus)", () => { + //#given / #when / #then expect(PLAN_AGENT_NAMES).toContain("plan") - expect(PLAN_AGENT_NAMES).toContain("prometheus") expect(PLAN_AGENT_NAMES).toContain("planner") + expect(PLAN_AGENT_NAMES).not.toContain("prometheus") }) }) @@ -2258,68 +2258,36 @@ describe("sisyphus-task", () => { expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills)) }) - test("prepends plan agent system prompt when agentName is 'prometheus'", () => { - // given + test("does not prepend plan agent prompt for prometheus agent", () => { + //#given - prometheus is NOT a plan agent (decoupled) const { buildSystemContent } = require("./tools") - const { buildPlanAgentSystemPrepend } = require("./constants") + const skillContent = "You are a strategic planner" - const availableCategories = [ - { - name: "ultrabrain", - description: "Complex architecture, deep logical reasoning", - model: "openai/gpt-5.3-codex", - }, - ] - const availableSkills = [ - { - name: "git-master", - description: "Atomic commits, git operations.", - location: "plugin", - }, - ] - - // when + //#when const result = buildSystemContent({ + skillContent, agentName: "prometheus", - availableCategories, - availableSkills, }) - // then - expect(result).toContain("") - expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills)) + //#then - prometheus should NOT get plan agent system prepend + expect(result).toBe(skillContent) + expect(result).not.toContain("MANDATORY CONTEXT GATHERING PROTOCOL") }) - test("prepends plan agent system prompt when agentName is 'Prometheus' (case insensitive)", () => { - // given + test("does not prepend plan agent prompt for Prometheus (case insensitive)", () => { + //#given - Prometheus (capitalized) is NOT a plan agent const { buildSystemContent } = require("./tools") - const { buildPlanAgentSystemPrepend } = require("./constants") + const skillContent = "You are a strategic planner" - const availableCategories = [ - { - name: "quick", - description: "Trivial tasks", - model: "anthropic/claude-haiku-4-5", - }, - ] - const availableSkills = [ - { - name: "dev-browser", - description: "Persistent browser state automation.", - location: "plugin", - }, - ] - - // when + //#when const result = buildSystemContent({ + skillContent, agentName: "Prometheus", - availableCategories, - availableSkills, }) - // then - expect(result).toContain("") - expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills)) + //#then + expect(result).toBe(skillContent) + expect(result).not.toContain("MANDATORY CONTEXT GATHERING PROTOCOL") }) test("combines plan agent prepend with skill content", () => { @@ -2565,14 +2533,14 @@ describe("sisyphus-task", () => { }) }) - describe("prometheus self-delegation block", () => { - test("prometheus cannot delegate to prometheus - returns error with guidance", async () => { - // given - current agent is prometheus + describe("plan agent self-delegation block", () => { + test("plan agent cannot delegate to plan - returns error with guidance", async () => { + //#given - current agent is plan const { createDelegateTask } = require("./tools") const mockManager = { launch: async () => ({}) } const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, session: { get: async () => ({ data: { directory: "/project" } }), @@ -2592,44 +2560,44 @@ describe("sisyphus-task", () => { const toolContext = { sessionID: "parent-session", messageID: "parent-message", - agent: "prometheus", + agent: "plan", abort: new AbortController().signal, } - // when - prometheus tries to delegate to prometheus + //#when - plan agent tries to delegate to plan const result = await tool.execute( { description: "Test self-delegation block", prompt: "Create a plan", - subagent_type: "prometheus", + subagent_type: "plan", run_in_background: false, load_skills: [], }, toolContext ) - // then - should return error telling prometheus to create plan directly - expect(result).toContain("prometheus") + //#then - should return error telling plan agent to create plan directly + expect(result).toContain("plan agent") expect(result).toContain("directly") }) - test("non-prometheus agent CAN delegate to prometheus - proceeds normally", async () => { - // given - current agent is sisyphus + test("prometheus is NOT a plan agent - can delegate to plan normally", async () => { + //#given - current agent is prometheus (no longer treated as plan agent) const { createDelegateTask } = require("./tools") const mockManager = { launch: async () => ({}) } const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, session: { get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_prometheus_allowed" } }), + create: async () => ({ data: { id: "ses_plan_from_prometheus" } }), prompt: async () => ({ data: {} }), promptAsync: async () => ({ data: {} }), messages: async () => ({ data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created successfully" }] }] }), - status: async () => ({ data: { "ses_prometheus_allowed": { type: "idle" } } }), + status: async () => ({ data: { "ses_plan_from_prometheus": { type: "idle" } } }), }, } @@ -2641,34 +2609,34 @@ describe("sisyphus-task", () => { const toolContext = { sessionID: "parent-session", messageID: "parent-message", - agent: "sisyphus", + agent: "prometheus", abort: new AbortController().signal, } - // when - sisyphus delegates to prometheus + //#when - prometheus delegates to plan (should work now) const result = await tool.execute( { - description: "Test prometheus delegation from non-prometheus agent", + description: "Test plan delegation from prometheus", prompt: "Create a plan", - subagent_type: "prometheus", + subagent_type: "plan", run_in_background: false, load_skills: [], }, toolContext ) - // then - should proceed normally + //#then - should proceed normally (prometheus is not plan agent) expect(result).not.toContain("Cannot delegate") expect(result).toContain("Plan created successfully") }, { timeout: 20000 }) - test("case-insensitive: Prometheus (capitalized) cannot delegate to prometheus", async () => { - // given - current agent is Prometheus (capitalized) + test("planner agent self-delegation is also blocked", async () => { + //#given - current agent is planner const { createDelegateTask } = require("./tools") const mockManager = { launch: async () => ({}) } const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + app: { agents: async () => ({ data: [{ name: "planner", mode: "subagent" }] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, session: { get: async () => ({ data: { directory: "/project" } }), @@ -2688,24 +2656,24 @@ describe("sisyphus-task", () => { const toolContext = { sessionID: "parent-session", messageID: "parent-message", - agent: "Prometheus", + agent: "planner", abort: new AbortController().signal, } - // when - Prometheus tries to delegate to prometheus + //#when - planner tries to delegate to plan const result = await tool.execute( { - description: "Test case-insensitive block", + description: "Test planner self-delegation block", prompt: "Create a plan", - subagent_type: "prometheus", + subagent_type: "plan", run_in_background: false, load_skills: [], }, toolContext ) - // then - should still return error - expect(result).toContain("prometheus") + //#then - should return error (planner is a plan agent alias) + expect(result).toContain("plan agent") expect(result).toContain("directly") }) }) @@ -2903,9 +2871,9 @@ describe("sisyphus-task", () => { }, { timeout: 20000 }) }) - describe("prometheus subagent task permission", () => { - test("prometheus subagent should have task permission enabled", async () => { - // given - sisyphus delegates to prometheus + describe("subagent task permission", () => { + test("plan subagent should have task permission enabled", async () => { + //#given - sisyphus delegates to plan agent const { createDelegateTask } = require("./tools") let promptBody: any @@ -2917,17 +2885,17 @@ describe("sisyphus-task", () => { } const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) }, config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, session: { get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_prometheus_delegate" } }), + create: async () => ({ data: { id: "ses_plan_delegate" } }), prompt: promptMock, promptAsync: promptMock, messages: async () => ({ data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }] }), - status: async () => ({ data: { "ses_prometheus_delegate": { type: "idle" } } }), + status: async () => ({ data: { "ses_plan_delegate": { type: "idle" } } }), }, } @@ -2943,10 +2911,65 @@ describe("sisyphus-task", () => { abort: new AbortController().signal, } - // when - sisyphus delegates to prometheus + //#when - sisyphus delegates to plan await tool.execute( { - description: "Test prometheus task permission", + description: "Test plan task permission", + prompt: "Create a plan", + subagent_type: "plan", + run_in_background: false, + load_skills: [], + }, + toolContext + ) + + //#then - plan agent should have task permission + expect(promptBody.tools.task).toBe(true) + }, { timeout: 20000 }) + + test("prometheus subagent should NOT have task permission (decoupled from plan)", async () => { + //#given - sisyphus delegates to prometheus (no longer a plan agent) + const { createDelegateTask } = require("./tools") + let promptBody: any + + const mockManager = { launch: async () => ({}) } + + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } + + const mockClient = { + app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_prometheus_no_task" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }] + }), + status: async () => ({ data: { "ses_prometheus_no_task": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + //#when - sisyphus delegates to prometheus + await tool.execute( + { + description: "Test prometheus no task permission", prompt: "Create a plan", subagent_type: "prometheus", run_in_background: false, @@ -2955,12 +2978,12 @@ describe("sisyphus-task", () => { toolContext ) - // then - prometheus should have task permission - expect(promptBody.tools.task).toBe(true) + //#then - prometheus should NOT have task permission (it's not a plan agent) + expect(promptBody.tools.task).toBe(false) }, { timeout: 20000 }) - test("non-prometheus subagent should NOT have task permission", async () => { - // given - sisyphus delegates to oracle (non-prometheus) + test("non-plan subagent should NOT have task permission", async () => { + //#given - sisyphus delegates to oracle (non-plan) const { createDelegateTask } = require("./tools") let promptBody: any