From 4a722df8be9f41f59cd0a537ac5c61812724f4a4 Mon Sep 17 00:00:00 2001 From: GeonWoo Jeon Date: Tue, 13 Jan 2026 23:06:58 +0900 Subject: [PATCH] feat(hooks): add sisyphus-task-retry hook for auto-correction Helps non-Opus models recover from sisyphus_task call failures: - Detects common errors (missing params, mutual exclusion, unknown values) - Injects retry guidance with correct parameter format - Extracts available options from error messages - Disableable via config: disabledHooks: ['sisyphus-task-retry'] --- src/config/schema.ts | 1 + src/hooks/index.ts | 1 + src/hooks/sisyphus-task-retry/index.test.ts | 119 +++++++++++++++++ src/hooks/sisyphus-task-retry/index.ts | 136 ++++++++++++++++++++ src/index.ts | 10 +- 5 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 src/hooks/sisyphus-task-retry/index.test.ts create mode 100644 src/hooks/sisyphus-task-retry/index.ts diff --git a/src/config/schema.ts b/src/config/schema.ts index dba799cb..4518d661 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -84,6 +84,7 @@ export const HookNameSchema = z.enum([ "claude-code-hooks", "auto-slash-command", "edit-error-recovery", + "sisyphus-task-retry", "prometheus-md-only", "start-work", "sisyphus-orchestrator", diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 642872e9..a8a1c85e 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -30,3 +30,4 @@ export { createPrometheusMdOnlyHook } from "./prometheus-md-only"; export { createTaskResumeInfoHook } from "./task-resume-info"; export { createStartWorkHook } from "./start-work"; export { createSisyphusOrchestratorHook } from "./sisyphus-orchestrator"; +export { createSisyphusTaskRetryHook } from "./sisyphus-task-retry"; diff --git a/src/hooks/sisyphus-task-retry/index.test.ts b/src/hooks/sisyphus-task-retry/index.test.ts new file mode 100644 index 00000000..c9899b46 --- /dev/null +++ b/src/hooks/sisyphus-task-retry/index.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "bun:test" +import { + SISYPHUS_TASK_ERROR_PATTERNS, + detectSisyphusTaskError, + buildRetryGuidance, +} from "./index" + +describe("sisyphus-task-retry", () => { + describe("SISYPHUS_TASK_ERROR_PATTERNS", () => { + // #given error patterns are defined + // #then should include all known sisyphus_task error types + it("should contain all known error patterns", () => { + expect(SISYPHUS_TASK_ERROR_PATTERNS.length).toBeGreaterThan(5) + + const patternTexts = SISYPHUS_TASK_ERROR_PATTERNS.map(p => p.pattern) + expect(patternTexts).toContain("run_in_background") + expect(patternTexts).toContain("skills") + expect(patternTexts).toContain("category OR subagent_type") + expect(patternTexts).toContain("Unknown category") + expect(patternTexts).toContain("Unknown agent") + }) + }) + + describe("detectSisyphusTaskError", () => { + // #given tool output with run_in_background error + // #when detecting error + // #then should return matching error info + it("should detect run_in_background missing error", () => { + const output = "❌ Invalid arguments: 'run_in_background' parameter is REQUIRED. Use run_in_background=false for task delegation." + + const result = detectSisyphusTaskError(output) + + expect(result).not.toBeNull() + expect(result?.errorType).toBe("missing_run_in_background") + }) + + it("should detect skills missing error", () => { + const output = "❌ Invalid arguments: 'skills' parameter is REQUIRED. Use skills=[] if no skills needed." + + const result = detectSisyphusTaskError(output) + + expect(result).not.toBeNull() + expect(result?.errorType).toBe("missing_skills") + }) + + it("should detect category/subagent mutual exclusion error", () => { + const output = "❌ Invalid arguments: Provide EITHER category OR subagent_type, not both." + + const result = detectSisyphusTaskError(output) + + expect(result).not.toBeNull() + expect(result?.errorType).toBe("mutual_exclusion") + }) + + it("should detect unknown category error", () => { + const output = '❌ Unknown category: "invalid-cat". Available: visual-engineering, ultrabrain, quick' + + const result = detectSisyphusTaskError(output) + + expect(result).not.toBeNull() + expect(result?.errorType).toBe("unknown_category") + }) + + it("should detect unknown agent error", () => { + const output = '❌ Unknown agent: "fake-agent". Available agents: explore, librarian, oracle' + + const result = detectSisyphusTaskError(output) + + expect(result).not.toBeNull() + expect(result?.errorType).toBe("unknown_agent") + }) + + it("should return null for successful output", () => { + const output = "Background task launched.\n\nTask ID: bg_12345\nSession ID: ses_abc" + + const result = detectSisyphusTaskError(output) + + expect(result).toBeNull() + }) + }) + + describe("buildRetryGuidance", () => { + // #given detected error + // #when building retry guidance + // #then should return actionable fix instructions + it("should provide fix for missing run_in_background", () => { + const errorInfo = { errorType: "missing_run_in_background", originalOutput: "" } + + const guidance = buildRetryGuidance(errorInfo) + + expect(guidance).toContain("run_in_background") + expect(guidance).toContain("REQUIRED") + }) + + it("should provide fix for unknown category with available list", () => { + const errorInfo = { + errorType: "unknown_category", + originalOutput: '❌ Unknown category: "bad". Available: visual-engineering, ultrabrain' + } + + const guidance = buildRetryGuidance(errorInfo) + + expect(guidance).toContain("visual-engineering") + expect(guidance).toContain("ultrabrain") + }) + + it("should provide fix for unknown agent with available list", () => { + const errorInfo = { + errorType: "unknown_agent", + originalOutput: '❌ Unknown agent: "fake". Available agents: explore, oracle' + } + + const guidance = buildRetryGuidance(errorInfo) + + expect(guidance).toContain("explore") + expect(guidance).toContain("oracle") + }) + }) +}) diff --git a/src/hooks/sisyphus-task-retry/index.ts b/src/hooks/sisyphus-task-retry/index.ts new file mode 100644 index 00000000..91b0645a --- /dev/null +++ b/src/hooks/sisyphus-task-retry/index.ts @@ -0,0 +1,136 @@ +import type { PluginInput } from "@opencode-ai/plugin" + +export interface SisyphusTaskErrorPattern { + pattern: string + errorType: string + fixHint: string +} + +export const SISYPHUS_TASK_ERROR_PATTERNS: SisyphusTaskErrorPattern[] = [ + { + pattern: "run_in_background", + errorType: "missing_run_in_background", + fixHint: "Add run_in_background=false (for delegation) or run_in_background=true (for parallel exploration)", + }, + { + pattern: "skills", + errorType: "missing_skills", + fixHint: "Add skills=[] parameter (empty array if no skills needed)", + }, + { + pattern: "category OR subagent_type", + errorType: "mutual_exclusion", + fixHint: "Provide ONLY one of: category (e.g., 'general', 'quick') OR subagent_type (e.g., 'oracle', 'explore')", + }, + { + pattern: "Must provide either category or subagent_type", + errorType: "missing_category_or_agent", + fixHint: "Add either category='general' OR subagent_type='explore'", + }, + { + pattern: "Unknown category", + errorType: "unknown_category", + fixHint: "Use a valid category from the Available list in the error message", + }, + { + pattern: "Agent name cannot be empty", + errorType: "empty_agent", + fixHint: "Provide a non-empty subagent_type value", + }, + { + pattern: "Unknown agent", + errorType: "unknown_agent", + fixHint: "Use a valid agent from the Available agents list in the error message", + }, + { + pattern: "Cannot call primary agent", + errorType: "primary_agent", + fixHint: "Primary agents cannot be called via sisyphus_task. Use a subagent like 'explore', 'oracle', or 'librarian'", + }, + { + pattern: "Skills not found", + errorType: "unknown_skills", + fixHint: "Use valid skill names from the Available list in the error message", + }, +] + +export interface DetectedError { + errorType: string + originalOutput: string +} + +export function detectSisyphusTaskError(output: string): DetectedError | null { + if (!output.includes("❌")) return null + + for (const errorPattern of SISYPHUS_TASK_ERROR_PATTERNS) { + if (output.includes(errorPattern.pattern)) { + return { + errorType: errorPattern.errorType, + originalOutput: output, + } + } + } + + return null +} + +function extractAvailableList(output: string): string | null { + const availableMatch = output.match(/Available[^:]*:\s*(.+)$/m) + return availableMatch ? availableMatch[1].trim() : null +} + +export function buildRetryGuidance(errorInfo: DetectedError): string { + const pattern = SISYPHUS_TASK_ERROR_PATTERNS.find( + (p) => p.errorType === errorInfo.errorType + ) + + if (!pattern) { + return `[sisyphus_task ERROR] Fix the error and retry with correct parameters.` + } + + let guidance = ` +[sisyphus_task CALL FAILED - IMMEDIATE RETRY REQUIRED] + +**Error Type**: ${errorInfo.errorType} +**Fix**: ${pattern.fixHint} +` + + const availableList = extractAvailableList(errorInfo.originalOutput) + if (availableList) { + guidance += `\n**Available Options**: ${availableList}\n` + } + + guidance += ` +**Action**: Retry sisyphus_task NOW with corrected parameters. + +Example of CORRECT call: +\`\`\` +sisyphus_task( + description="Task description", + prompt="Detailed prompt...", + category="general", // OR subagent_type="explore" + run_in_background=false, + skills=[] +) +\`\`\` +` + + return guidance +} + +export function createSisyphusTaskRetryHook(_ctx: PluginInput) { + return { + "tool.execute.after": async ( + input: { tool: string; sessionID: string; callID: string }, + output: { title: string; output: string; metadata: unknown } + ) => { + if (input.tool.toLowerCase() !== "sisyphus_task") return + + const errorInfo = detectSisyphusTaskError(output.output) + if (errorInfo) { + const guidance = buildRetryGuidance(errorInfo) + output.output += `\n${guidance}` + } + }, + } +} diff --git a/src/index.ts b/src/index.ts index 4380e1a8..db0b2426 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import { createRalphLoopHook, createAutoSlashCommandHook, createEditErrorRecoveryHook, + createSisyphusTaskRetryHook, createTaskResumeInfoHook, createStartWorkHook, createSisyphusOrchestratorHook, @@ -202,6 +203,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { ? createEditErrorRecoveryHook(ctx) : null; + const sisyphusTaskRetry = isHookEnabled("sisyphus-task-retry") + ? createSisyphusTaskRetryHook(ctx) + : null; + const startWork = isHookEnabled("start-work") ? createStartWorkHook(ctx) : null; @@ -554,8 +559,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { await emptyTaskResponseDetector?.["tool.execute.after"](input, output); await agentUsageReminder?.["tool.execute.after"](input, output); await interactiveBashSession?.["tool.execute.after"](input, output); - await editErrorRecovery?.["tool.execute.after"](input, output); - await sisyphusOrchestrator?.["tool.execute.after"]?.(input, output); +await editErrorRecovery?.["tool.execute.after"](input, output); + await sisyphusTaskRetry?.["tool.execute.after"](input, output); + await sisyphusOrchestrator?.["tool.execute.after"]?.(input, output); await taskResumeInfo["tool.execute.after"](input, output); }, };