Browse Source

Merge branch 'dev'

Alvin Unreal 1 month ago
parent
commit
36b16f7446

+ 41 - 0
src/hooks/delegate-task-retry/guidance.ts

@@ -0,0 +1,41 @@
+import { DELEGATE_TASK_ERROR_PATTERNS, type DetectedError } from './patterns';
+
+function extractAvailableList(output: string): string | null {
+  const match = output.match(/Allowed agents:\s*(.+)$/m);
+  if (match) return match[1].trim();
+
+  const available = output.match(/Available[^:]*:\s*(.+)$/m);
+  if (available) return available[1].trim();
+
+  return null;
+}
+
+export function buildRetryGuidance(errorInfo: DetectedError): string {
+  const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
+    (p) => p.errorType === errorInfo.errorType,
+  );
+
+  if (!pattern) {
+    return '\n[delegate-task retry] Fix parameters and retry with corrected arguments.';
+  }
+
+  const available = extractAvailableList(errorInfo.originalOutput);
+
+  const lines = [
+    '',
+    '[delegate-task retry suggestion]',
+    `Error type: ${errorInfo.errorType}`,
+    `Fix: ${pattern.fixHint}`,
+  ];
+
+  if (available) {
+    lines.push(`Available: ${available}`);
+  }
+
+  lines.push(
+    'Retry now with corrected parameters. Example:',
+    'task(description="...", prompt="...", category="unspecified-low", run_in_background=false, load_skills=[])',
+  );
+
+  return lines.join('\n');
+}

+ 23 - 0
src/hooks/delegate-task-retry/hook.ts

@@ -0,0 +1,23 @@
+import type { PluginInput } from '@opencode-ai/plugin';
+import { buildRetryGuidance } from './guidance';
+import { detectDelegateTaskError } from './patterns';
+
+export function createDelegateTaskRetryHook(_ctx: PluginInput) {
+  return {
+    'tool.execute.after': async (
+      input: { tool: string },
+      output: { output: unknown },
+    ): Promise<void> => {
+      const toolName = input.tool.toLowerCase();
+      const isDelegateTool = toolName === 'task' || toolName === 'background_task';
+      if (!isDelegateTool) return;
+
+      if (typeof output.output !== 'string') return;
+
+      const detected = detectDelegateTaskError(output.output);
+      if (!detected) return;
+
+      output.output += `\n${buildRetryGuidance(detected)}`;
+    },
+  };
+}

+ 38 - 0
src/hooks/delegate-task-retry/index.test.ts

@@ -0,0 +1,38 @@
+import { describe, expect, test } from 'bun:test';
+import { createDelegateTaskRetryHook } from './hook';
+
+describe('delegate-task-retry hook', () => {
+  test('appends guidance for task argument errors', async () => {
+    const hook = createDelegateTaskRetryHook({} as never);
+    const output = {
+      output:
+        '[ERROR] Invalid arguments: Must provide either category or subagent_type. Available categories: quick, unspecified-low',
+    };
+
+    await hook['tool.execute.after']({ tool: 'task' }, output);
+
+    expect(output.output).toContain('[delegate-task retry suggestion]');
+    expect(output.output).toContain('missing_category_or_agent');
+  });
+
+  test('appends guidance for background agent allowlist errors', async () => {
+    const hook = createDelegateTaskRetryHook({} as never);
+    const output = {
+      output: "Agent 'oracle' is not allowed. Allowed agents: explorer, fixer",
+    };
+
+    await hook['tool.execute.after']({ tool: 'background_task' }, output);
+
+    expect(output.output).toContain('background_agent_not_allowed');
+    expect(output.output).toContain('Available: explorer, fixer');
+  });
+
+  test('does nothing for unrelated tool output', async () => {
+    const hook = createDelegateTaskRetryHook({} as never);
+    const output = { output: 'all good' };
+
+    await hook['tool.execute.after']({ tool: 'read' }, output);
+
+    expect(output.output).toBe('all good');
+  });
+});

+ 4 - 0
src/hooks/delegate-task-retry/index.ts

@@ -0,0 +1,4 @@
+export type { DelegateTaskErrorPattern, DetectedError } from './patterns';
+export { DELEGATE_TASK_ERROR_PATTERNS, detectDelegateTaskError } from './patterns';
+export { buildRetryGuidance } from './guidance';
+export { createDelegateTaskRetryHook } from './hook';

+ 81 - 0
src/hooks/delegate-task-retry/patterns.ts

@@ -0,0 +1,81 @@
+export interface DelegateTaskErrorPattern {
+  pattern: string;
+  errorType: string;
+  fixHint: string;
+}
+
+export const DELEGATE_TASK_ERROR_PATTERNS: DelegateTaskErrorPattern[] = [
+  {
+    pattern: 'run_in_background',
+    errorType: 'missing_run_in_background',
+    fixHint:
+      'Add run_in_background=false (delegation) or run_in_background=true (parallel exploration).',
+  },
+  {
+    pattern: 'load_skills',
+    errorType: 'missing_load_skills',
+    fixHint:
+      'Add load_skills=[] (empty array when no skill is needed).',
+  },
+  {
+    pattern: 'category OR subagent_type',
+    errorType: 'mutual_exclusion',
+    fixHint:
+      'Provide only one: category (e.g., "unspecified-low") OR subagent_type (e.g., "explorer").',
+  },
+  {
+    pattern: 'Must provide either category or subagent_type',
+    errorType: 'missing_category_or_agent',
+    fixHint:
+      'Add either category="unspecified-low" or subagent_type="explorer".',
+  },
+  {
+    pattern: 'Unknown category',
+    errorType: 'unknown_category',
+    fixHint:
+      'Use a valid category listed in the error output.',
+  },
+  {
+    pattern: 'Unknown agent',
+    errorType: 'unknown_agent',
+    fixHint: 'Use a valid agent name from the available list.',
+  },
+  {
+    pattern: 'Skills not found',
+    errorType: 'unknown_skills',
+    fixHint: 'Use valid skill names listed in the error output.',
+  },
+  {
+    pattern: 'is not allowed. Allowed agents:',
+    errorType: 'background_agent_not_allowed',
+    fixHint:
+      'Use one of the allowed agents shown in the error or delegate from a parent agent that can call this subagent.',
+  },
+];
+
+export interface DetectedError {
+  errorType: string;
+  originalOutput: string;
+}
+
+export function detectDelegateTaskError(output: string): DetectedError | null {
+  if (!output || typeof output !== 'string') return null;
+
+  const hasErrorSignal =
+    output.includes('[ERROR]') ||
+    output.includes('Invalid arguments') ||
+    output.includes('is not allowed. Allowed agents:');
+
+  if (!hasErrorSignal) return null;
+
+  for (const pattern of DELEGATE_TASK_ERROR_PATTERNS) {
+    if (output.includes(pattern.pattern)) {
+      return {
+        errorType: pattern.errorType,
+        originalOutput: output,
+      };
+    }
+  }
+
+  return null;
+}

+ 1 - 0
src/hooks/index.ts

@@ -1,4 +1,5 @@
 export type { AutoUpdateCheckerOptions } from './auto-update-checker';
 export { createAutoUpdateCheckerHook } from './auto-update-checker';
+export { createDelegateTaskRetryHook } from './delegate-task-retry';
 export { createPhaseReminderHook } from './phase-reminder';
 export { createPostReadNudgeHook } from './post-read-nudge';

+ 24 - 2
src/index.ts

@@ -5,6 +5,7 @@ import { loadPluginConfig, type TmuxConfig } from './config';
 import { parseList } from './config/agent-mcps';
 import {
   createAutoUpdateCheckerHook,
+  createDelegateTaskRetryHook,
   createPhaseReminderHook,
   createPostReadNudgeHook,
 } from './hooks';
@@ -68,6 +69,9 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   // Initialize post-read nudge hook
   const postReadNudgeHook = createPostReadNudgeHook();
 
+  // Initialize delegate-task retry guidance hook
+  const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
+
   return {
     name: 'oh-my-opencode-slim',
 
@@ -201,8 +205,26 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     'experimental.chat.messages.transform':
       phaseReminderHook['experimental.chat.messages.transform'],
 
-    // Nudge after file reads to encourage delegation
-    'tool.execute.after': postReadNudgeHook['tool.execute.after'],
+    // Post-tool hooks: retry guidance for delegation errors + post-read nudge
+    'tool.execute.after': async (input, output) => {
+      await delegateTaskRetryHook['tool.execute.after'](
+        input as { tool: string },
+        output as { output: unknown },
+      );
+
+      await postReadNudgeHook['tool.execute.after'](
+        input as {
+          tool: string;
+          sessionID?: string;
+          callID?: string;
+        },
+        output as {
+          title: string;
+          output: string;
+          metadata: Record<string, unknown>;
+        },
+      );
+    },
   };
 };