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:
parent
c6fb5e58c8
commit
4a722df8be
@ -84,6 +84,7 @@ export const HookNameSchema = z.enum([
|
|||||||
"claude-code-hooks",
|
"claude-code-hooks",
|
||||||
"auto-slash-command",
|
"auto-slash-command",
|
||||||
"edit-error-recovery",
|
"edit-error-recovery",
|
||||||
|
"sisyphus-task-retry",
|
||||||
"prometheus-md-only",
|
"prometheus-md-only",
|
||||||
"start-work",
|
"start-work",
|
||||||
"sisyphus-orchestrator",
|
"sisyphus-orchestrator",
|
||||||
|
|||||||
@ -30,3 +30,4 @@ export { createPrometheusMdOnlyHook } from "./prometheus-md-only";
|
|||||||
export { createTaskResumeInfoHook } from "./task-resume-info";
|
export { createTaskResumeInfoHook } from "./task-resume-info";
|
||||||
export { createStartWorkHook } from "./start-work";
|
export { createStartWorkHook } from "./start-work";
|
||||||
export { createSisyphusOrchestratorHook } from "./sisyphus-orchestrator";
|
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,
|
createRalphLoopHook,
|
||||||
createAutoSlashCommandHook,
|
createAutoSlashCommandHook,
|
||||||
createEditErrorRecoveryHook,
|
createEditErrorRecoveryHook,
|
||||||
|
createSisyphusTaskRetryHook,
|
||||||
createTaskResumeInfoHook,
|
createTaskResumeInfoHook,
|
||||||
createStartWorkHook,
|
createStartWorkHook,
|
||||||
createSisyphusOrchestratorHook,
|
createSisyphusOrchestratorHook,
|
||||||
@ -202,6 +203,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
? createEditErrorRecoveryHook(ctx)
|
? createEditErrorRecoveryHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const sisyphusTaskRetry = isHookEnabled("sisyphus-task-retry")
|
||||||
|
? createSisyphusTaskRetryHook(ctx)
|
||||||
|
: null;
|
||||||
|
|
||||||
const startWork = isHookEnabled("start-work")
|
const startWork = isHookEnabled("start-work")
|
||||||
? createStartWorkHook(ctx)
|
? createStartWorkHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
@ -554,8 +559,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||||
await agentUsageReminder?.["tool.execute.after"](input, output);
|
await agentUsageReminder?.["tool.execute.after"](input, output);
|
||||||
await interactiveBashSession?.["tool.execute.after"](input, output);
|
await interactiveBashSession?.["tool.execute.after"](input, output);
|
||||||
await editErrorRecovery?.["tool.execute.after"](input, output);
|
await editErrorRecovery?.["tool.execute.after"](input, output);
|
||||||
await sisyphusOrchestrator?.["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);
|
await taskResumeInfo["tool.execute.after"](input, output);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user