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']
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
119
src/hooks/sisyphus-task-retry/index.test.ts
Normal file
119
src/hooks/sisyphus-task-retry/index.test.ts
Normal file
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
136
src/hooks/sisyphus-task-retry/index.ts
Normal file
136
src/hooks/sisyphus-task-retry/index.ts
Normal file
@@ -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}`
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
10
src/index.ts
10
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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user