From 1d0135b230896539f8c6ec3b022cb9824a8f2eb4 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 31 Mar 2026 13:05:44 -0700 Subject: [PATCH] fix(delegate-task): reject stray backend-style categories Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../prometheus-gpt-category-prompt.test.ts | 14 ++++++ src/agents/prometheus/gpt.ts | 2 +- ...category-resolver-unknown-category.test.ts | 43 +++++++++++++++++++ src/tools/delegate-task/category-resolver.ts | 18 +++++++- src/tools/delegate-task/task-schema.test.ts | 28 ++++++++++++ src/tools/delegate-task/tools.ts | 2 +- 6 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/agents/prometheus-gpt-category-prompt.test.ts create mode 100644 src/tools/delegate-task/category-resolver-unknown-category.test.ts create mode 100644 src/tools/delegate-task/task-schema.test.ts diff --git a/src/agents/prometheus-gpt-category-prompt.test.ts b/src/agents/prometheus-gpt-category-prompt.test.ts new file mode 100644 index 000000000..249c14365 --- /dev/null +++ b/src/agents/prometheus-gpt-category-prompt.test.ts @@ -0,0 +1,14 @@ +declare const require: (name: string) => any +const { describe, expect, test } = require("bun:test") +import { PROMETHEUS_GPT_SYSTEM_PROMPT } from "./prometheus/gpt" + +describe("PROMETHEUS_GPT_SYSTEM_PROMPT category guidance", () => { + test("#given recommended agent profile instructions #when reading category placeholder #then it must point planners at available categories rather than a free-form name", () => { + //#given + const prompt = PROMETHEUS_GPT_SYSTEM_PROMPT + + //#when / #then + expect(prompt).not.toContain("Category: `[name]`") + expect(prompt).toContain("Category: `[category-from-available-categories-above]`") + }) +}) diff --git a/src/agents/prometheus/gpt.ts b/src/agents/prometheus/gpt.ts index 578ddb149..a16f564d9 100644 --- a/src/agents/prometheus/gpt.ts +++ b/src/agents/prometheus/gpt.ts @@ -363,7 +363,7 @@ Wave 2: [dependent tasks with categories] **Must NOT do**: [specific exclusions] **Recommended Agent Profile**: - - Category: \`[name]\` — Reason: [why] + - Category: \`[category-from-available-categories-above]\` — Reason: [why] - Skills: [\`skill-1\`] — [why needed] - Omitted: [\`skill-x\`] — [why not needed] diff --git a/src/tools/delegate-task/category-resolver-unknown-category.test.ts b/src/tools/delegate-task/category-resolver-unknown-category.test.ts new file mode 100644 index 000000000..5a12235f6 --- /dev/null +++ b/src/tools/delegate-task/category-resolver-unknown-category.test.ts @@ -0,0 +1,43 @@ +declare const require: (name: string) => any +const { afterEach, beforeEach, describe, expect, mock, spyOn, test } = require("bun:test") +import { resolveCategoryExecution } from "./category-resolver" +import type { ExecutorContext } from "./executor-types" +import * as availableModels from "./available-models" + +describe("resolveCategoryExecution unknown category handling", () => { + beforeEach(() => { + mock.restore() + }) + + afterEach(() => { + mock.restore() + }) + + test("#given unknown category #when resolving category execution #then it rejects before fetching available models", async () => { + //#given + const availableModelsSpy = spyOn(availableModels, "getAvailableModelsForDelegateTask") + const executorContext: ExecutorContext = { + client: {} as ExecutorContext["client"], + manager: {} as ExecutorContext["manager"], + directory: "/tmp/test", + userCategories: {}, + sisyphusJuniorModel: undefined, + } + const args = { + category: "backend-engineer", + prompt: "test prompt", + description: "Test task", + run_in_background: false, + load_skills: [], + blockedBy: undefined, + enableSkillTools: false, + } + + //#when + const result = await resolveCategoryExecution(args, executorContext, undefined, "anthropic/claude-sonnet-4-6") + + //#then + expect(result.error).toContain('Unknown category: "backend-engineer"') + expect(availableModelsSpy).not.toHaveBeenCalled() + }) +}) diff --git a/src/tools/delegate-task/category-resolver.ts b/src/tools/delegate-task/category-resolver.ts index 6fb667d4e..e7099c604 100644 --- a/src/tools/delegate-task/category-resolver.ts +++ b/src/tools/delegate-task/category-resolver.ts @@ -45,12 +45,26 @@ export async function resolveCategoryExecution( ): Promise { const { client, userCategories, sisyphusJuniorModel } = executorCtx - const availableModels = await getAvailableModelsForDelegateTask(client) - const categoryName = args.category! const enabledCategories = mergeCategories(userCategories) const categoryExists = enabledCategories[categoryName] !== undefined + if (!categoryExists) { + const allCategoryNames = Object.keys(enabledCategories).join(", ") + return { + agentToUse: "", + categoryModel: undefined, + categoryPromptAppend: undefined, + maxPromptTokens: undefined, + modelInfo: undefined, + actualModel: undefined, + isUnstableAgent: false, + error: `Unknown category: "${categoryName}". Available: ${allCategoryNames}`, + } + } + + const availableModels = await getAvailableModelsForDelegateTask(client) + const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, diff --git a/src/tools/delegate-task/task-schema.test.ts b/src/tools/delegate-task/task-schema.test.ts new file mode 100644 index 000000000..00be7fc43 --- /dev/null +++ b/src/tools/delegate-task/task-schema.test.ts @@ -0,0 +1,28 @@ +declare const require: (name: string) => any +const { describe, expect, test } = require("bun:test") +import { createDelegateTask } from "./tools" + + describe("createDelegateTask schema", () => { + test("#given category arg #when tool is created #then category is constrained to available enum values", () => { + //#given + const toolDefinition = createDelegateTask({ manager: {} as never, client: {} as never, directory: "/tmp/test" }) + + //#when + const categorySchema = toolDefinition.args.category as unknown as { + def: { + type: string + innerType: { + def: { type: string } + options: string[] + } + } + } + + //#then + expect(categorySchema.def.type).toBe("optional") + expect(categorySchema.def.innerType.def.type).toBe("enum") + expect(categorySchema.def.innerType.options).toContain("quick") + expect(categorySchema.def.innerType.options).toContain("deep") + expect(categorySchema.def.innerType.options).toContain("ultrabrain") + }) +}) diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index f07a0c7bc..929b5c2f9 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -101,7 +101,7 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini description: tool.schema.string().describe("Short task description (3-5 words)"), prompt: tool.schema.string().describe("Full detailed prompt for the agent"), run_in_background: tool.schema.boolean().describe("REQUIRED. true=async (returns task_id), false=sync (waits). Use false for task delegation, true ONLY for parallel exploration."), - category: tool.schema.string().optional().describe(`REQUIRED if subagent_type not provided. Do NOT provide both category and subagent_type.`), + category: tool.schema.enum(categoryNames).optional().describe(`REQUIRED if subagent_type not provided. Do NOT provide both category and subagent_type.`), subagent_type: tool.schema.string().optional().describe("REQUIRED if category not provided. Do NOT provide both category and subagent_type. Must be a callable non-primary agent name returned by app.agents()."), session_id: tool.schema.string().optional().describe("Existing Task session to continue"), command: tool.schema.string().optional().describe("The command that triggered this task"),