diff --git a/src/tools/sisyphus-task/constants.ts b/src/tools/sisyphus-task/constants.ts
new file mode 100644
index 00000000..c468cf7d
--- /dev/null
+++ b/src/tools/sisyphus-task/constants.ts
@@ -0,0 +1,249 @@
+import type { CategoryConfig } from "../../config/schema"
+
+export const VISUAL_CATEGORY_PROMPT_APPEND = `
+You are working on VISUAL/UI tasks.
+
+Design-first mindset:
+- Bold aesthetic choices over safe defaults
+- Unexpected layouts, asymmetry, grid-breaking elements
+- Distinctive typography (avoid: Arial, Inter, Roboto, Space Grotesk)
+- Cohesive color palettes with sharp accents
+- High-impact animations with staggered reveals
+- Atmosphere: gradient meshes, noise textures, layered transparencies
+
+AVOID: Generic fonts, purple gradients on white, predictable layouts, cookie-cutter patterns.
+`
+
+export const STRATEGIC_CATEGORY_PROMPT_APPEND = `
+You are working on BUSINESS LOGIC / ARCHITECTURE tasks.
+
+Strategic advisor mindset:
+- Bias toward simplicity: least complex solution that fulfills requirements
+- Leverage existing code/patterns over new components
+- Prioritize developer experience and maintainability
+- One clear recommendation with effort estimate (Quick/Short/Medium/Large)
+- Signal when advanced approach warranted
+
+Response format:
+- Bottom line (2-3 sentences)
+- Action plan (numbered steps)
+- Risks and mitigations (if relevant)
+`
+
+export const ARTISTRY_CATEGORY_PROMPT_APPEND = `
+You are working on HIGHLY CREATIVE / ARTISTIC tasks.
+
+Artistic genius mindset:
+- Push far beyond conventional boundaries
+- Explore radical, unconventional directions
+- Surprise and delight: unexpected twists, novel combinations
+- Rich detail and vivid expression
+- Break patterns deliberately when it serves the creative vision
+
+Approach:
+- Generate diverse, bold options first
+- Embrace ambiguity and wild experimentation
+- Balance novelty with coherence
+- This is for tasks requiring exceptional creativity
+`
+
+export const QUICK_CATEGORY_PROMPT_APPEND = `
+You are working on SMALL / QUICK tasks.
+
+Efficient execution mindset:
+- Fast, focused, minimal overhead
+- Get to the point immediately
+- No over-engineering
+- Simple solutions for simple problems
+
+Approach:
+- Minimal viable implementation
+- Skip unnecessary abstractions
+- Direct and concise
+
+
+
+⚠️ THIS CATEGORY USES A LESS CAPABLE MODEL (claude-haiku-4-5).
+
+The model executing this task has LIMITED reasoning capacity. Your prompt MUST be:
+
+**EXHAUSTIVELY EXPLICIT** - Leave NOTHING to interpretation:
+1. MUST DO: List every required action as atomic, numbered steps
+2. MUST NOT DO: Explicitly forbid likely mistakes and deviations
+3. EXPECTED OUTPUT: Describe exact success criteria with concrete examples
+
+**WHY THIS MATTERS:**
+- Less capable models WILL deviate without explicit guardrails
+- Vague instructions → unpredictable results
+- Implicit expectations → missed requirements
+
+**PROMPT STRUCTURE (MANDATORY):**
+\`\`\`
+TASK: [One-sentence goal]
+
+MUST DO:
+1. [Specific action with exact details]
+2. [Another specific action]
+...
+
+MUST NOT DO:
+- [Forbidden action + why]
+- [Another forbidden action]
+...
+
+EXPECTED OUTPUT:
+- [Exact deliverable description]
+- [Success criteria / verification method]
+\`\`\`
+
+If your prompt lacks this structure, REWRITE IT before delegating.
+`
+
+export const MOST_CAPABLE_CATEGORY_PROMPT_APPEND = `
+You are working on COMPLEX / MOST-CAPABLE tasks.
+
+Maximum capability mindset:
+- Bring full reasoning power to bear
+- Consider all edge cases and implications
+- Deep analysis before action
+- Quality over speed
+
+Approach:
+- Thorough understanding first
+- Comprehensive solution design
+- Meticulous execution
+- This is for the most challenging problems
+`
+
+export const WRITING_CATEGORY_PROMPT_APPEND = `
+You are working on WRITING / PROSE tasks.
+
+Wordsmith mindset:
+- Clear, flowing prose
+- Appropriate tone and voice
+- Engaging and readable
+- Proper structure and organization
+
+Approach:
+- Understand the audience
+- Draft with care
+- Polish for clarity and impact
+- Documentation, READMEs, articles, technical writing
+`
+
+export const GENERAL_CATEGORY_PROMPT_APPEND = `
+You are working on GENERAL tasks.
+
+Balanced execution mindset:
+- Practical, straightforward approach
+- Good enough is good enough
+- Focus on getting things done
+
+Approach:
+- Standard best practices
+- Reasonable trade-offs
+- Efficient completion
+
+
+
+⚠️ THIS CATEGORY USES A MID-TIER MODEL (claude-sonnet-4-5).
+
+While capable, this model benefits significantly from EXPLICIT instructions.
+
+**PROVIDE CLEAR STRUCTURE:**
+1. MUST DO: Enumerate required actions explicitly - don't assume inference
+2. MUST NOT DO: State forbidden actions to prevent scope creep or wrong approaches
+3. EXPECTED OUTPUT: Define concrete success criteria and deliverables
+
+**COMMON PITFALLS WITHOUT EXPLICIT INSTRUCTIONS:**
+- Model may take shortcuts that miss edge cases
+- Implicit requirements get overlooked
+- Output format may not match expectations
+- Scope may expand beyond intended boundaries
+
+**RECOMMENDED PROMPT PATTERN:**
+\`\`\`
+TASK: [Clear, single-purpose goal]
+
+CONTEXT: [Relevant background the model needs]
+
+MUST DO:
+- [Explicit requirement 1]
+- [Explicit requirement 2]
+
+MUST NOT DO:
+- [Boundary/constraint 1]
+- [Boundary/constraint 2]
+
+EXPECTED OUTPUT:
+- [What success looks like]
+- [How to verify completion]
+\`\`\`
+
+The more explicit your prompt, the better the results.
+`
+
+export const DEFAULT_CATEGORIES: Record = {
+ "visual-engineering": {
+ model: "google/gemini-3-pro-preview",
+ temperature: 0.7,
+ },
+ "high-iq": {
+ model: "openai/gpt-5.2",
+ temperature: 0.1,
+ },
+ artistry: {
+ model: "google/gemini-3-pro-preview",
+ temperature: 0.9,
+ },
+ quick: {
+ model: "anthropic/claude-haiku-4-5",
+ temperature: 0.3,
+ },
+ "most-capable": {
+ model: "anthropic/claude-opus-4-5",
+ temperature: 0.1,
+ },
+ writing: {
+ model: "google/gemini-3-flash-preview",
+ temperature: 0.5,
+ },
+ general: {
+ model: "anthropic/claude-sonnet-4-5",
+ temperature: 0.3,
+ },
+}
+
+export const CATEGORY_PROMPT_APPENDS: Record = {
+ "visual-engineering": VISUAL_CATEGORY_PROMPT_APPEND,
+ "high-iq": STRATEGIC_CATEGORY_PROMPT_APPEND,
+ artistry: ARTISTRY_CATEGORY_PROMPT_APPEND,
+ quick: QUICK_CATEGORY_PROMPT_APPEND,
+ "most-capable": MOST_CAPABLE_CATEGORY_PROMPT_APPEND,
+ writing: WRITING_CATEGORY_PROMPT_APPEND,
+ general: GENERAL_CATEGORY_PROMPT_APPEND,
+}
+
+export const CATEGORY_DESCRIPTIONS: Record = {
+ "visual-engineering": "Frontend, UI/UX, design, styling, animation",
+ "high-iq": "Strict architecture design, very complex business logic",
+ artistry: "Highly creative/artistic tasks, novel ideas",
+ quick: "Cheap & fast - small tasks with minimal overhead, budget-friendly",
+ "most-capable": "Complex tasks requiring maximum capability",
+ writing: "Documentation, prose, technical writing",
+ general: "General purpose tasks",
+}
+
+const BUILTIN_CATEGORIES = Object.keys(DEFAULT_CATEGORIES).join(", ")
+
+export const SISYPHUS_TASK_DESCRIPTION = `Spawn agent task with category-based or direct agent selection.
+
+MUTUALLY EXCLUSIVE: Provide EITHER category OR agent, not both (unless resuming).
+
+- category: Use predefined category (${BUILTIN_CATEGORIES}) → Spawns Sisyphus-Junior with category config
+- agent: Use specific agent directly (e.g., "oracle", "explore")
+- background: true=async (returns task_id), false=sync (waits for result). Default: false. Use background=true ONLY for parallel exploration with 5+ independent queries.
+- resume: Task ID to resume - continues previous agent session with full context preserved
+- skills: Array of skill names to prepend to prompt (e.g., ["playwright", "frontend-ui-ux"]). Skills will be resolved and their content prepended with a separator. Empty array = no prepending.
+
+Prompts MUST be in English.`
diff --git a/src/tools/sisyphus-task/index.ts b/src/tools/sisyphus-task/index.ts
new file mode 100644
index 00000000..bbbe3f58
--- /dev/null
+++ b/src/tools/sisyphus-task/index.ts
@@ -0,0 +1,3 @@
+export { createSisyphusTask, type SisyphusTaskToolOptions } from "./tools"
+export type * from "./types"
+export * from "./constants"
diff --git a/src/tools/sisyphus-task/tools.test.ts b/src/tools/sisyphus-task/tools.test.ts
new file mode 100644
index 00000000..05964e57
--- /dev/null
+++ b/src/tools/sisyphus-task/tools.test.ts
@@ -0,0 +1,217 @@
+import { describe, test, expect } from "bun:test"
+import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, SISYPHUS_TASK_DESCRIPTION } from "./constants"
+import type { CategoryConfig } from "../../config/schema"
+
+function resolveCategoryConfig(
+ categoryName: string,
+ userCategories?: Record
+): { config: CategoryConfig; promptAppend: string } | null {
+ const defaultConfig = DEFAULT_CATEGORIES[categoryName]
+ const userConfig = userCategories?.[categoryName]
+ const defaultPromptAppend = CATEGORY_PROMPT_APPENDS[categoryName] ?? ""
+
+ if (!defaultConfig && !userConfig) {
+ return null
+ }
+
+ const config: CategoryConfig = {
+ ...defaultConfig,
+ ...userConfig,
+ model: userConfig?.model ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5",
+ }
+
+ let promptAppend = defaultPromptAppend
+ if (userConfig?.prompt_append) {
+ promptAppend = defaultPromptAppend
+ ? defaultPromptAppend + "\n\n" + userConfig.prompt_append
+ : userConfig.prompt_append
+ }
+
+ return { config, promptAppend }
+}
+
+describe("sisyphus-task", () => {
+ describe("DEFAULT_CATEGORIES", () => {
+ test("visual-engineering category has gemini model", () => {
+ // #given
+ const category = DEFAULT_CATEGORIES["visual-engineering"]
+
+ // #when / #then
+ expect(category).toBeDefined()
+ expect(category.model).toBe("google/gemini-3-pro-preview")
+ expect(category.temperature).toBe(0.7)
+ })
+
+ test("high-iq category has gpt model", () => {
+ // #given
+ const category = DEFAULT_CATEGORIES["high-iq"]
+
+ // #when / #then
+ expect(category).toBeDefined()
+ expect(category.model).toBe("openai/gpt-5.2")
+ expect(category.temperature).toBe(0.1)
+ })
+ })
+
+ describe("CATEGORY_PROMPT_APPENDS", () => {
+ test("visual-engineering category has design-focused prompt", () => {
+ // #given
+ const promptAppend = CATEGORY_PROMPT_APPENDS["visual-engineering"]
+
+ // #when / #then
+ expect(promptAppend).toContain("VISUAL/UI")
+ expect(promptAppend).toContain("Design-first")
+ })
+
+ test("high-iq category has strategic prompt", () => {
+ // #given
+ const promptAppend = CATEGORY_PROMPT_APPENDS["high-iq"]
+
+ // #when / #then
+ expect(promptAppend).toContain("BUSINESS LOGIC")
+ expect(promptAppend).toContain("Strategic advisor")
+ })
+ })
+
+ describe("CATEGORY_DESCRIPTIONS", () => {
+ test("has description for all default categories", () => {
+ // #given
+ const defaultCategoryNames = Object.keys(DEFAULT_CATEGORIES)
+
+ // #when / #then
+ for (const name of defaultCategoryNames) {
+ expect(CATEGORY_DESCRIPTIONS[name]).toBeDefined()
+ expect(CATEGORY_DESCRIPTIONS[name].length).toBeGreaterThan(0)
+ }
+ })
+
+ test("most-capable category exists and has description", () => {
+ // #given / #when
+ const description = CATEGORY_DESCRIPTIONS["most-capable"]
+
+ // #then
+ expect(description).toBeDefined()
+ expect(description).toContain("Complex")
+ })
+ })
+
+ describe("SISYPHUS_TASK_DESCRIPTION", () => {
+ test("documents background parameter as required with default false", () => {
+ // #given / #when / #then
+ expect(SISYPHUS_TASK_DESCRIPTION).toContain("background")
+ expect(SISYPHUS_TASK_DESCRIPTION).toContain("Default: false")
+ })
+
+ test("warns about parallel exploration usage", () => {
+ // #given / #when / #then
+ expect(SISYPHUS_TASK_DESCRIPTION).toContain("5+")
+ })
+ })
+
+ describe("resolveCategoryConfig", () => {
+ test("returns null for unknown category without user config", () => {
+ // #given
+ const categoryName = "unknown-category"
+
+ // #when
+ const result = resolveCategoryConfig(categoryName)
+
+ // #then
+ expect(result).toBeNull()
+ })
+
+ test("returns default config for builtin category", () => {
+ // #given
+ const categoryName = "visual-engineering"
+
+ // #when
+ const result = resolveCategoryConfig(categoryName)
+
+ // #then
+ expect(result).not.toBeNull()
+ expect(result!.config.model).toBe("google/gemini-3-pro-preview")
+ expect(result!.promptAppend).toContain("VISUAL/UI")
+ })
+
+ test("user config overrides default model", () => {
+ // #given
+ const categoryName = "visual-engineering"
+ const userCategories = {
+ "visual-engineering": { model: "anthropic/claude-opus-4-5" },
+ }
+
+ // #when
+ const result = resolveCategoryConfig(categoryName, userCategories)
+
+ // #then
+ expect(result).not.toBeNull()
+ expect(result!.config.model).toBe("anthropic/claude-opus-4-5")
+ })
+
+ test("user prompt_append is appended to default", () => {
+ // #given
+ const categoryName = "visual-engineering"
+ const userCategories = {
+ "visual-engineering": {
+ model: "google/gemini-3-pro-preview",
+ prompt_append: "Custom instructions here",
+ },
+ }
+
+ // #when
+ const result = resolveCategoryConfig(categoryName, userCategories)
+
+ // #then
+ expect(result).not.toBeNull()
+ expect(result!.promptAppend).toContain("VISUAL/UI")
+ expect(result!.promptAppend).toContain("Custom instructions here")
+ })
+
+ test("user can define custom category", () => {
+ // #given
+ const categoryName = "my-custom"
+ const userCategories = {
+ "my-custom": {
+ model: "openai/gpt-5.2",
+ temperature: 0.5,
+ prompt_append: "You are a custom agent",
+ },
+ }
+
+ // #when
+ const result = resolveCategoryConfig(categoryName, userCategories)
+
+ // #then
+ expect(result).not.toBeNull()
+ expect(result!.config.model).toBe("openai/gpt-5.2")
+ expect(result!.config.temperature).toBe(0.5)
+ expect(result!.promptAppend).toBe("You are a custom agent")
+ })
+
+ test("user category overrides temperature", () => {
+ // #given
+ const categoryName = "visual-engineering"
+ const userCategories = {
+ "visual-engineering": {
+ model: "google/gemini-3-pro-preview",
+ temperature: 0.3,
+ },
+ }
+
+ // #when
+ const result = resolveCategoryConfig(categoryName, userCategories)
+
+ // #then
+ expect(result).not.toBeNull()
+ expect(result!.config.temperature).toBe(0.3)
+ })
+ })
+
+ describe("skills parameter", () => {
+ test("SISYPHUS_TASK_DESCRIPTION documents skills parameter", () => {
+ // #given / #when / #then
+ expect(SISYPHUS_TASK_DESCRIPTION).toContain("skills")
+ expect(SISYPHUS_TASK_DESCRIPTION).toContain("Array of skill names")
+ })
+ })
+})
diff --git a/src/tools/sisyphus-task/tools.ts b/src/tools/sisyphus-task/tools.ts
new file mode 100644
index 00000000..7481e503
--- /dev/null
+++ b/src/tools/sisyphus-task/tools.ts
@@ -0,0 +1,312 @@
+import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
+import { existsSync, readdirSync } from "node:fs"
+import { join } from "node:path"
+import type { BackgroundManager } from "../../features/background-agent"
+import type { SisyphusTaskArgs } from "./types"
+import type { CategoryConfig, CategoriesConfig } from "../../config/schema"
+import { SISYPHUS_TASK_DESCRIPTION, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS } from "./constants"
+import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
+import { resolveMultipleSkills } from "../../features/opencode-skill-loader/skill-content"
+import { createBuiltinSkills } from "../../features/builtin-skills/skills"
+
+type OpencodeClient = PluginInput["client"]
+
+const SISYPHUS_JUNIOR_AGENT = "Sisyphus-Junior"
+const CATEGORY_EXAMPLES = Object.keys(DEFAULT_CATEGORIES).map(k => `'${k}'`).join(", ")
+
+function parseModelString(model: string): { providerID: string; modelID: string } | undefined {
+ const parts = model.split("/")
+ if (parts.length >= 2) {
+ return { providerID: parts[0], modelID: parts.slice(1).join("/") }
+ }
+ return undefined
+}
+
+function getMessageDir(sessionID: string): string | null {
+ if (!existsSync(MESSAGE_STORAGE)) return null
+
+ const directPath = join(MESSAGE_STORAGE, sessionID)
+ if (existsSync(directPath)) return directPath
+
+ for (const dir of readdirSync(MESSAGE_STORAGE)) {
+ const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
+ if (existsSync(sessionPath)) return sessionPath
+ }
+
+ return null
+}
+
+function formatDuration(start: Date, end?: Date): string {
+ const duration = (end ?? new Date()).getTime() - start.getTime()
+ const seconds = Math.floor(duration / 1000)
+ const minutes = Math.floor(seconds / 60)
+ const hours = Math.floor(minutes / 60)
+
+ if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`
+ return `${seconds}s`
+}
+
+type ToolContextWithMetadata = {
+ sessionID: string
+ messageID: string
+ agent: string
+ abort: AbortSignal
+ metadata?: (input: { title?: string; metadata?: Record }) => void
+}
+
+function resolveCategoryConfig(
+ categoryName: string,
+ userCategories?: CategoriesConfig
+): { config: CategoryConfig; promptAppend: string } | null {
+ const defaultConfig = DEFAULT_CATEGORIES[categoryName]
+ const userConfig = userCategories?.[categoryName]
+ const defaultPromptAppend = CATEGORY_PROMPT_APPENDS[categoryName] ?? ""
+
+ if (!defaultConfig && !userConfig) {
+ return null
+ }
+
+ const config: CategoryConfig = {
+ ...defaultConfig,
+ ...userConfig,
+ model: userConfig?.model ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5",
+ }
+
+ let promptAppend = defaultPromptAppend
+ if (userConfig?.prompt_append) {
+ promptAppend = defaultPromptAppend
+ ? defaultPromptAppend + "\n\n" + userConfig.prompt_append
+ : userConfig.prompt_append
+ }
+
+ return { config, promptAppend }
+}
+
+export interface SisyphusTaskToolOptions {
+ manager: BackgroundManager
+ client: OpencodeClient
+ userCategories?: CategoriesConfig
+}
+
+export function createSisyphusTask(options: SisyphusTaskToolOptions): ToolDefinition {
+ const { manager, client, userCategories } = options
+
+ return tool({
+ description: SISYPHUS_TASK_DESCRIPTION,
+ args: {
+ description: tool.schema.string().describe("Short task description"),
+ prompt: tool.schema.string().describe("Full detailed prompt for the agent"),
+ category: tool.schema.string().optional().describe(`Category name (e.g., ${CATEGORY_EXAMPLES}). Mutually exclusive with agent.`),
+ agent: tool.schema.string().optional().describe("Agent name directly (e.g., 'oracle', 'explore'). Mutually exclusive with category."),
+ background: tool.schema.boolean().describe("Run in background. MUST be explicitly set. Use false for task delegation, true only for parallel exploration."),
+ resume: tool.schema.string().optional().describe("Session ID to resume - continues previous agent session with full context"),
+ skills: tool.schema.array(tool.schema.string()).optional().describe("Array of skill names to prepend to the prompt. Skills will be resolved and their content prepended with a separator."),
+ },
+ async execute(args: SisyphusTaskArgs, toolContext) {
+ const ctx = toolContext as ToolContextWithMetadata
+ if (args.background === undefined) {
+ return `❌ Invalid arguments: 'background' parameter is REQUIRED. Use background=false for task delegation, background=true only for parallel exploration.`
+ }
+ const runInBackground = args.background === true
+
+ // Handle skills - resolve and prepend to prompt
+ if (args.skills && args.skills.length > 0) {
+ const { resolved, notFound } = resolveMultipleSkills(args.skills)
+ if (notFound.length > 0) {
+ const available = createBuiltinSkills().map(s => s.name).join(", ")
+ return `❌ Skills not found: ${notFound.join(", ")}. Available: ${available}`
+ }
+ const skillContent = Array.from(resolved.values()).join("\n\n")
+ args.prompt = skillContent + "\n\n---\n\n" + args.prompt
+ }
+
+ const messageDir = getMessageDir(ctx.sessionID)
+ const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
+ const parentModel = prevMessage?.model?.providerID && prevMessage?.model?.modelID
+ ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID }
+ : undefined
+
+ // Handle resume case first
+ if (args.resume) {
+ try {
+ const task = await manager.resume({
+ sessionId: args.resume,
+ prompt: args.prompt,
+ parentSessionID: ctx.sessionID,
+ parentMessageID: ctx.messageID,
+ parentModel,
+ })
+
+ ctx.metadata?.({
+ title: `Resume: ${task.description}`,
+ metadata: { sessionId: task.sessionID },
+ })
+
+ return `Background task resumed.
+
+Task ID: ${task.id}
+Session ID: ${task.sessionID}
+Description: ${task.description}
+Agent: ${task.agent}
+Status: ${task.status}
+
+Agent continues with full previous context preserved.
+Use \`background_output\` with task_id="${task.id}" to check progress.`
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error)
+ return `❌ Failed to resume task: ${message}`
+ }
+ }
+
+ if (args.category && args.agent) {
+ return `❌ Invalid arguments: Provide EITHER category OR agent, not both.`
+ }
+
+ if (!args.category && !args.agent) {
+ return `❌ Invalid arguments: Must provide either category or agent.`
+ }
+
+ let agentToUse: string
+
+ let categoryModel: { providerID: string; modelID: string } | undefined
+
+ if (args.category) {
+ const resolved = resolveCategoryConfig(args.category, userCategories)
+ if (!resolved) {
+ return `❌ Unknown category: "${args.category}". Available: ${Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories }).join(", ")}`
+ }
+
+ agentToUse = SISYPHUS_JUNIOR_AGENT
+ categoryModel = parseModelString(resolved.config.model)
+ } else {
+ agentToUse = args.agent!.trim()
+ if (!agentToUse) {
+ return `❌ Agent name cannot be empty.`
+ }
+
+ // Validate agent exists and is callable (not a primary agent)
+ try {
+ const agentsResult = await client.app.agents()
+ type AgentInfo = { name: string; mode?: "subagent" | "primary" | "all" }
+ const agents = (agentsResult as { data?: AgentInfo[] }).data ?? agentsResult as unknown as AgentInfo[]
+
+ const callableAgents = agents.filter((a) => a.mode !== "primary")
+ const callableNames = callableAgents.map((a) => a.name)
+
+ if (!callableNames.includes(agentToUse)) {
+ const isPrimaryAgent = agents.some((a) => a.name === agentToUse && a.mode === "primary")
+ if (isPrimaryAgent) {
+ return `❌ Cannot call primary agent "${agentToUse}" via sisyphus_task. Primary agents are top-level orchestrators.`
+ }
+
+ const availableAgents = callableNames
+ .sort()
+ .join(", ")
+ return `❌ Unknown agent: "${agentToUse}". Available agents: ${availableAgents}`
+ }
+ } catch {
+ // If we can't fetch agents, proceed anyway - the session.prompt will fail with a clearer error
+ }
+ }
+
+ if (runInBackground) {
+ try {
+ const task = await manager.launch({
+ description: args.description,
+ prompt: args.prompt,
+ agent: agentToUse,
+ parentSessionID: ctx.sessionID,
+ parentMessageID: ctx.messageID,
+ parentModel,
+ model: categoryModel,
+ })
+
+ ctx.metadata?.({
+ title: args.description,
+ metadata: { sessionId: task.sessionID, category: args.category },
+ })
+
+ return `Background task launched.
+
+Task ID: ${task.id}
+Session ID: ${task.sessionID}
+Description: ${task.description}
+Agent: ${task.agent}${args.category ? ` (category: ${args.category})` : ""}
+Status: ${task.status}
+
+System notifies on completion. Use \`background_output\` with task_id="${task.id}" to check.`
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error)
+ return `❌ Failed to launch task: ${message}`
+ }
+ }
+
+ try {
+ const createResult = await client.session.create({
+ body: {
+ parentID: ctx.sessionID,
+ title: `Task: ${args.description}`,
+ },
+ })
+
+ if (createResult.error) {
+ return `❌ Failed to create session: ${createResult.error}`
+ }
+
+ const sessionID = createResult.data.id
+ const startTime = new Date()
+
+ ctx.metadata?.({
+ title: args.description,
+ metadata: { sessionId: sessionID, category: args.category, sync: true },
+ })
+
+ await client.session.prompt({
+ path: { id: sessionID },
+ body: {
+ agent: agentToUse,
+ model: categoryModel,
+ tools: {
+ task: false,
+ sisyphus_task: false,
+ },
+ parts: [{ type: "text", text: args.prompt }],
+ },
+ })
+
+ const messagesResult = await client.session.messages({
+ path: { id: sessionID },
+ })
+
+ if (messagesResult.error) {
+ return `❌ Error fetching result: ${messagesResult.error}`
+ }
+
+ const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as Array<{
+ info?: { role?: string }
+ parts?: Array<{ type?: string; text?: string }>
+ }>
+
+ const assistantMessages = messages.filter((m) => m.info?.role === "assistant")
+ const lastMessage = assistantMessages[assistantMessages.length - 1]
+ const textParts = lastMessage?.parts?.filter((p) => p.type === "text") ?? []
+ const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n")
+
+ const duration = formatDuration(startTime)
+
+ return `Task completed in ${duration}.
+
+Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""}
+Session ID: ${sessionID}
+
+---
+
+${textContent || "(No text output)"}`
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error)
+ return `❌ Task failed: ${message}`
+ }
+ },
+ })
+}
diff --git a/src/tools/sisyphus-task/types.ts b/src/tools/sisyphus-task/types.ts
new file mode 100644
index 00000000..d6581e31
--- /dev/null
+++ b/src/tools/sisyphus-task/types.ts
@@ -0,0 +1,9 @@
+export interface SisyphusTaskArgs {
+ description: string
+ prompt: string
+ category?: string
+ agent?: string
+ background: boolean
+ resume?: string
+ skills?: string[]
+}