Jelajahi Sumber

Merge remote-tracking branch 'origin/master' into fallback-consolidation

# Conflicts:
#	oh-my-opencode-slim.schema.json
#	src/config/loader.test.ts
#	src/config/schema.ts
Alvin Unreal 1 bulan lalu
induk
melakukan
817fa01202

+ 2 - 194
oh-my-opencode-slim.schema.json

@@ -8,197 +8,10 @@
     "setDefaultAgent": {
       "type": "boolean"
     },
-    "scoringEngineVersion": {
-      "type": "string",
-      "enum": [
-        "v1",
-        "v2-shadow",
-        "v2"
-      ]
-    },
-    "balanceProviderUsage": {
-      "type": "boolean"
-    },
     "autoUpdate": {
       "description": "Disable automatic installation of plugin updates when false. Defaults to true.",
       "type": "boolean"
     },
-    "manualPlan": {
-      "type": "object",
-      "properties": {
-        "orchestrator": {
-          "type": "object",
-          "properties": {
-            "primary": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback1": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback2": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback3": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            }
-          },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
-        },
-        "oracle": {
-          "type": "object",
-          "properties": {
-            "primary": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback1": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback2": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback3": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            }
-          },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
-        },
-        "designer": {
-          "type": "object",
-          "properties": {
-            "primary": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback1": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback2": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback3": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            }
-          },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
-        },
-        "explorer": {
-          "type": "object",
-          "properties": {
-            "primary": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback1": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback2": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback3": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            }
-          },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
-        },
-        "librarian": {
-          "type": "object",
-          "properties": {
-            "primary": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback1": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback2": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback3": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            }
-          },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
-        },
-        "fixer": {
-          "type": "object",
-          "properties": {
-            "primary": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback1": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback2": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            },
-            "fallback3": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$"
-            }
-          },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
-        }
-      },
-      "required": [
-        "orchestrator",
-        "oracle",
-        "designer",
-        "explorer",
-        "librarian",
-        "fixer"
-      ],
-      "additionalProperties": false
-    },
     "presets": {
       "type": "object",
       "propertyNames": {
@@ -539,7 +352,8 @@
           "description": "When true (default), empty provider responses are treated as failures, triggering fallback/retry. Set to false to treat them as successes.",
           "type": "boolean"
         }
-      }
+      },
+      "additionalProperties": false
     },
     "council": {
       "type": "object",
@@ -590,12 +404,6 @@
         },
         "master": {
           "description": "DEPRECATED — ignored. Council agent synthesizes directly."
-        },
-        "master_timeout": {
-          "description": "DEPRECATED — ignored. Use \"timeout\" instead."
-        },
-        "master_fallback": {
-          "description": "DEPRECATED — ignored. No separate master session."
         }
       },
       "required": [

+ 0 - 3
src/agents/orchestrator.ts

@@ -264,9 +264,6 @@ When user's approach seems problematic:
 `;
 }
 
-/** @deprecated Use buildOrchestratorPrompt() instead */
-export const ORCHESTRATOR_PROMPT = buildOrchestratorPrompt();
-
 export function createOrchestratorAgent(
   model?: string | Array<string | { id: string; variant?: string }>,
   customPrompt?: string,

+ 1 - 15
src/config/agent-mcps.ts

@@ -1,9 +1,4 @@
-import {
-  type AgentName,
-  getAgentOverride,
-  McpNameSchema,
-  type PluginConfig,
-} from '.';
+import { type AgentName, getAgentOverride, type PluginConfig } from '.';
 
 /** Default MCPs per agent - "*" means all MCPs, "!item" excludes specific MCPs */
 
@@ -43,15 +38,6 @@ export function parseList(items: string[], allAvailable: string[]): string[] {
   );
 }
 
-/**
- * Get available MCP names from schema and config.
- */
-export function getAvailableMcpNames(config?: PluginConfig): string[] {
-  const builtinMcps = McpNameSchema.options;
-  const disabled = new Set(config?.disabled_mcps ?? []);
-  return builtinMcps.filter((name) => !disabled.has(name));
-}
-
 /**
  * Get the MCP list for an agent (from config or defaults).
  */

+ 1 - 42
src/config/constants.ts

@@ -15,31 +15,11 @@ export const SUBAGENT_NAMES = [
   'councillor',
 ] as const;
 
-export const ORCHESTRATOR_NAME = 'orchestrator' as const;
-
-export const ALL_AGENT_NAMES = [ORCHESTRATOR_NAME, ...SUBAGENT_NAMES] as const;
+export const ALL_AGENT_NAMES = ['orchestrator', ...SUBAGENT_NAMES] as const;
 
 // Agent name type (for use in DEFAULT_MODELS)
 export type AgentName = (typeof ALL_AGENT_NAMES)[number];
 
-// Subagent delegation rules: which agents can spawn which subagents
-// orchestrator: can spawn all subagents (full delegation)
-// fixer: leaf node — prompt forbids delegation; use grep/glob for lookups
-// designer: can spawn explorer (for research during design)
-// explorer/librarian/oracle: cannot spawn any subagents (leaf nodes)
-// Unknown agent types not listed here default to explorer-only access
-// Which agents each agent type can spawn via delegation.
-// councillor is internal — only CouncilManager spawns it.
-export const ORCHESTRATABLE_AGENTS = [
-  'explorer',
-  'librarian',
-  'oracle',
-  'designer',
-  'fixer',
-  'observer',
-  'council',
-] as const;
-
 /** Agents that cannot be disabled even if listed in disabled_agents config. */
 export const PROTECTED_AGENTS = new Set(['orchestrator', 'councillor']);
 
@@ -47,24 +27,6 @@ export const PROTECTED_AGENTS = new Set(['orchestrator', 'councillor']);
  * Get the list of orchestratable agents, excluding any disabled agents.
  * This is used for delegation validation at runtime.
  */
-export function getOrchestratableAgents(
-  disabledAgents?: Set<string>,
-): string[] {
-  return ORCHESTRATABLE_AGENTS.filter((name) => !disabledAgents?.has(name));
-}
-
-export const SUBAGENT_DELEGATION_RULES: Record<AgentName, readonly string[]> = {
-  orchestrator: ORCHESTRATABLE_AGENTS,
-  fixer: [],
-  designer: [],
-  explorer: [],
-  librarian: [],
-  oracle: [],
-  observer: [],
-  council: [],
-  councillor: [],
-};
-
 // Default models for each agent
 // orchestrator is undefined so its model is fully resolved at runtime via priority fallback
 export const DEFAULT_MODELS: Record<AgentName, string | undefined> = {
@@ -81,13 +43,10 @@ export const DEFAULT_MODELS: Record<AgentName, string | undefined> = {
 
 // Polling configuration
 export const POLL_INTERVAL_MS = 500;
-export const POLL_INTERVAL_SLOW_MS = 1000;
 export const POLL_INTERVAL_BACKGROUND_MS = 2000;
 
 // Timeouts
-export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
 export const MAX_POLL_TIME_MS = 5 * 60 * 1000; // 5 minutes
-export const FALLBACK_FAILOVER_TIMEOUT_MS = 15_000;
 
 // Subagent depth limits
 export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;

+ 2 - 10
src/config/council-schema.test.ts

@@ -37,11 +37,7 @@ describe('CouncillorConfigSchema', () => {
 
     if (result.success) {
       // Deprecated fields are stripped but reported via _deprecated
-      expect(result.data._deprecated).toEqual([
-        'master',
-        'master_timeout',
-        'master_fallback',
-      ]);
+      expect(result.data._deprecated).toEqual(['master']);
       // Core fields still work normally
       expect(result.data.timeout).toBe(180000);
       expect(Object.keys(result.data.presets.default)).toEqual(['alpha']);
@@ -147,11 +143,7 @@ test('deprecated master with non-standard model ID still parses', () => {
   expect(result.success).toBe(true);
 
   if (result.success) {
-    expect(result.data._deprecated).toEqual([
-      'master',
-      'master_timeout',
-      'master_fallback',
-    ]);
+    expect(result.data._deprecated).toEqual(['master']);
     // Even non-standard model IDs are extracted as-is for backward compat
     expect(result.data._legacyMasterModel).toBe('claude-opus-4-6');
   }

+ 0 - 30
src/config/council-schema.ts

@@ -151,21 +151,11 @@ export const CouncilConfigSchema = z
       .unknown()
       .optional()
       .describe('DEPRECATED — ignored. Council agent synthesizes directly.'),
-    master_timeout: z
-      .unknown()
-      .optional()
-      .describe('DEPRECATED — ignored. Use "timeout" instead.'),
-    master_fallback: z
-      .unknown()
-      .optional()
-      .describe('DEPRECATED — ignored. No separate master session.'),
   })
   .transform((data) => {
     // Detect deprecated fields and attach warning for consumers
     const deprecated: string[] = [];
     if (data.master !== undefined) deprecated.push('master');
-    if (data.master_timeout !== undefined) deprecated.push('master_timeout');
-    if (data.master_fallback !== undefined) deprecated.push('master_fallback');
 
     // Backward compat: extract master.model so the council agent can use it
     // as a fallback when no explicit council entry exists in the active preset.
@@ -194,26 +184,6 @@ export type CouncillorExecutionMode = z.infer<
   typeof CouncillorExecutionModeSchema
 >;
 
-/**
- * A sensible default council configuration that users can copy into their
- * opencode.jsonc. Provides a 3-councillor preset using common models.
- *
- * Users should replace models with ones they have access to.
- *
- * ```jsonc
- * "council": DEFAULT_COUNCIL_CONFIG
- * ```
- */
-export const DEFAULT_COUNCIL_CONFIG: z.input<typeof CouncilConfigSchema> = {
-  presets: {
-    default: {
-      alpha: { model: 'openai/gpt-5.4-mini' },
-      beta: { model: 'openai/gpt-5.3-codex' },
-      gamma: { model: 'google/gemini-3-pro' },
-    },
-  },
-};
-
 /**
  * Result of a council session.
  */

+ 29 - 84
src/config/loader.test.ts

@@ -51,36 +51,6 @@ describe('loadPluginConfig', () => {
     expect(config.agents?.oracle?.model).toBe('test/model');
   });
 
-  test('loads scoringEngineVersion flag when configured', () => {
-    const projectDir = path.join(tempDir, 'project');
-    const projectConfigDir = path.join(projectDir, '.opencode');
-    fs.mkdirSync(projectConfigDir, { recursive: true });
-    fs.writeFileSync(
-      path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
-      JSON.stringify({
-        scoringEngineVersion: 'v2-shadow',
-      }),
-    );
-
-    const config = loadPluginConfig(projectDir);
-    expect(config.scoringEngineVersion).toBe('v2-shadow');
-  });
-
-  test('loads balanceProviderUsage flag when configured', () => {
-    const projectDir = path.join(tempDir, 'project');
-    const projectConfigDir = path.join(projectDir, '.opencode');
-    fs.mkdirSync(projectConfigDir, { recursive: true });
-    fs.writeFileSync(
-      path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
-      JSON.stringify({
-        balanceProviderUsage: true,
-      }),
-    );
-
-    const config = loadPluginConfig(projectDir);
-    expect(config.balanceProviderUsage).toBe(true);
-  });
-
   test('loads autoUpdate flag when configured', () => {
     const projectDir = path.join(tempDir, 'project');
     const projectConfigDir = path.join(projectDir, '.opencode');
@@ -96,60 +66,6 @@ describe('loadPluginConfig', () => {
     expect(config.autoUpdate).toBe(false);
   });
 
-  test('loads manual plan structure when configured', () => {
-    const projectDir = path.join(tempDir, 'project');
-    const projectConfigDir = path.join(projectDir, '.opencode');
-    fs.mkdirSync(projectConfigDir, { recursive: true });
-    fs.writeFileSync(
-      path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
-      JSON.stringify({
-        manualPlan: {
-          orchestrator: {
-            primary: 'openai/gpt-5.5',
-            fallback1: 'anthropic/claude-opus-4-6',
-            fallback2: 'chutes/kimi-k2.5',
-            fallback3: 'opencode/gpt-5-nano',
-          },
-          oracle: {
-            primary: 'openai/gpt-5.5',
-            fallback1: 'anthropic/claude-opus-4-6',
-            fallback2: 'chutes/Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8-TEE',
-            fallback3: 'opencode/gpt-5-nano',
-          },
-          designer: {
-            primary: 'openai/gpt-5.5',
-            fallback1: 'anthropic/claude-opus-4-6',
-            fallback2: 'chutes/kimi-k2.5',
-            fallback3: 'opencode/gpt-5-nano',
-          },
-          explorer: {
-            primary: 'openai/gpt-5.5',
-            fallback1: 'anthropic/claude-opus-4-6',
-            fallback2: 'chutes/kimi-k2.5',
-            fallback3: 'opencode/gpt-5-nano',
-          },
-          librarian: {
-            primary: 'openai/gpt-5.5',
-            fallback1: 'anthropic/claude-opus-4-6',
-            fallback2: 'chutes/kimi-k2.5',
-            fallback3: 'opencode/gpt-5-nano',
-          },
-          fixer: {
-            primary: 'openai/gpt-5.5',
-            fallback1: 'anthropic/claude-opus-4-6',
-            fallback2: 'chutes/kimi-k2.5',
-            fallback3: 'opencode/gpt-5-nano',
-          },
-        },
-      }),
-    );
-
-    const config = loadPluginConfig(projectDir);
-    expect(config.manualPlan?.oracle?.fallback2).toBe(
-      'chutes/Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8-TEE',
-    );
-  });
-
   test('ignores invalid config (schema violation or malformed JSON)', () => {
     const projectDir = path.join(tempDir, 'project');
     const projectConfigDir = path.join(projectDir, '.opencode');
@@ -636,6 +552,35 @@ describe('deepMerge behavior', () => {
     const config = loadPluginConfig(projectDir);
     expect(config.agents?.oracle?.model).toBe('user/model');
   });
+
+  test('merges fallback timeout from user and project', () => {
+    const userOpencodeDir = path.join(userConfigDir, 'opencode');
+    fs.mkdirSync(userOpencodeDir, { recursive: true });
+    fs.writeFileSync(
+      path.join(userOpencodeDir, 'oh-my-opencode-slim.json'),
+      JSON.stringify({
+        fallback: {
+          enabled: true,
+        },
+      }),
+    );
+
+    const projectDir = path.join(tempDir, 'project');
+    const projectConfigDir = path.join(projectDir, '.opencode');
+    fs.mkdirSync(projectConfigDir, { recursive: true });
+    fs.writeFileSync(
+      path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
+      JSON.stringify({
+        fallback: {
+          enabled: false,
+        },
+      }),
+    );
+
+    const config = loadPluginConfig(projectDir);
+    // Fallback deepMerge: project value wins over user value
+    expect(config.fallback?.enabled).toBe(false);
+  });
 });
 
 describe('preset resolution', () => {

+ 0 - 20
src/config/runtime-preset.test.ts

@@ -2,7 +2,6 @@ import { describe, expect, test } from 'bun:test';
 import {
   getActiveRuntimePreset,
   getPreviousRuntimePreset,
-  rollbackRuntimePreset,
   setActiveRuntimePreset,
   setActiveRuntimePresetWithPrevious,
 } from './runtime-preset';
@@ -39,23 +38,4 @@ describe('runtime-preset', () => {
     expect(getPreviousRuntimePreset()).toBe('old');
     setActiveRuntimePreset(null);
   });
-
-  test('rollbackRuntimePreset restores active and clears previous', () => {
-    setActiveRuntimePreset(null);
-    setActiveRuntimePreset('old');
-    setActiveRuntimePresetWithPrevious('new');
-    rollbackRuntimePreset('old');
-    expect(getActiveRuntimePreset()).toBe('old');
-    expect(getPreviousRuntimePreset()).toBeNull();
-    setActiveRuntimePreset(null);
-  });
-
-  test('rollbackRuntimePreset with null clears active and previous', () => {
-    setActiveRuntimePreset(null);
-    setActiveRuntimePresetWithPrevious('new');
-    rollbackRuntimePreset(null);
-    expect(getActiveRuntimePreset()).toBeNull();
-    expect(getPreviousRuntimePreset()).toBeNull();
-    setActiveRuntimePreset(null);
-  });
 });

+ 0 - 5
src/config/runtime-preset.ts

@@ -30,8 +30,3 @@ export function setActiveRuntimePresetWithPrevious(name: string | null): void {
   previousRuntimePreset = activeRuntimePreset;
   activeRuntimePreset = name;
 }
-
-export function rollbackRuntimePreset(previous: string | null): void {
-  activeRuntimePreset = previous;
-  previousRuntimePreset = null;
-}

+ 0 - 3
src/config/schema.ts

@@ -233,15 +233,12 @@ export const PluginConfigSchema = z
   .object({
     preset: z.string().optional(),
     setDefaultAgent: z.boolean().optional(),
-    scoringEngineVersion: z.enum(['v1', 'v2-shadow', 'v2']).optional(),
-    balanceProviderUsage: z.boolean().optional(),
     autoUpdate: z
       .boolean()
       .optional()
       .describe(
         'Disable automatic installation of plugin updates when false. Defaults to true.',
       ),
-    manualPlan: ManualPlanSchema.optional(),
     presets: z.record(z.string(), PresetSchema).optional(),
     agents: z.record(z.string(), AgentOverrideConfigSchema).optional(),
     disabled_agents: z

+ 0 - 9
src/hooks/apply-patch/patch.ts

@@ -1,9 +0,0 @@
-// Compatibility shim for local deep imports; keep only the stable runtime
-// surface here. `applyPreparedChanges()` remains in operations.ts as an
-// internal best-effort helper for local tests/helpers.
-export { parsePatch } from './codec';
-export {
-  preparePatchChanges,
-  rewritePatch,
-  rewritePatchText,
-} from './operations';

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

@@ -1,41 +0,0 @@
-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');
-}

+ 45 - 2
src/hooks/delegate-task-retry/hook.ts

@@ -1,6 +1,49 @@
 import type { PluginInput } from '@opencode-ai/plugin';
-import { buildRetryGuidance } from './guidance';
-import { detectDelegateTaskError } from './patterns';
+import {
+  DELEGATE_TASK_ERROR_PATTERNS,
+  type DetectedError,
+  detectDelegateTaskError,
+} 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;
+}
+
+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');
+}
 
 export function createDelegateTaskRetryHook(_ctx: PluginInput) {
   return {

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

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

+ 1 - 16
src/hooks/filter-available-skills/index.ts

@@ -6,22 +6,7 @@
 import type { PluginInput } from '@opencode-ai/plugin';
 import { getSkillPermissionsForAgent } from '../../cli/skills';
 import { getAgentOverride, type PluginConfig } from '../../config';
-
-interface MessageInfo {
-  role: string;
-  agent?: string;
-}
-
-interface MessagePart {
-  type: string;
-  text?: string;
-  [key: string]: unknown;
-}
-
-interface MessageWithParts {
-  info: MessageInfo;
-  parts: MessagePart[];
-}
+import type { MessageWithParts } from '../types';
 
 const AVAILABLE_SKILLS_BLOCK_REGEX =
   /<available_skills>\s*([\s\S]*?)\s*<\/available_skills>/g;

+ 5 - 10
src/hooks/foreground-fallback/index.ts

@@ -16,7 +16,10 @@
 
 import type { PluginInput } from '@opencode-ai/plugin';
 import { log } from '../../utils/logger';
-import { abortSessionWithTimeout } from '../../utils/session';
+import {
+  abortSessionWithTimeout,
+  parseModelReference,
+} from '../../utils/session';
 
 type OpencodeClient = PluginInput['client'];
 
@@ -59,14 +62,6 @@ export function isRateLimitError(error: unknown): boolean {
 // Helpers
 // ---------------------------------------------------------------------------
 
-function parseModel(
-  model: string,
-): { providerID: string; modelID: string } | null {
-  const slash = model.indexOf('/');
-  if (slash <= 0 || slash >= model.length - 1) return null;
-  return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
-}
-
 /** Prevent re-triggering within this window for the same session. */
 const DEDUP_WINDOW_MS = 5_000;
 const REPROMPT_DELAY_MS = 500;
@@ -255,7 +250,7 @@ export class ForegroundFallbackManager {
       }
       tried.add(nextModel);
 
-      const ref = parseModel(nextModel);
+      const ref = parseModelReference(nextModel);
       if (!ref) {
         log('[foreground-fallback] invalid model format', {
           sessionID,

+ 1 - 9
src/hooks/image-hook.ts

@@ -9,6 +9,7 @@ import {
   writeFileSync,
 } from 'node:fs';
 import { basename, extname, join } from 'node:path';
+import type { MessageWithParts } from './types';
 
 // Debounce: only run cleanup every 10 minutes per directory
 const lastCleanupByDir = new Map<string, number>();
@@ -23,15 +24,6 @@ interface ImagePart {
   [key: string]: unknown;
 }
 
-interface MessageWithParts {
-  info: { role: string; agent?: string; sessionID?: string };
-  parts: Array<{
-    type: string;
-    text?: string;
-    [key: string]: unknown;
-  }>;
-}
-
 function isImagePart(p: ImagePart): boolean {
   if (p.type === 'image') return true;
   if (p.type === 'file') {

+ 2 - 2
src/hooks/index.ts

@@ -3,14 +3,14 @@ export type { AutoUpdateCheckerOptions } from './auto-update-checker';
 export { createAutoUpdateCheckerHook } from './auto-update-checker';
 export { createChatHeadersHook } from './chat-headers';
 export { createDeepworkCommandHook } from './deepwork';
-export { createDelegateTaskRetryHook } from './delegate-task-retry';
+export { createDelegateTaskRetryHook } from './delegate-task-retry/hook';
 export { createFilterAvailableSkillsHook } from './filter-available-skills';
 export {
   ForegroundFallbackManager,
   isRateLimitError,
 } from './foreground-fallback';
 export { processImageAttachments } from './image-hook';
-export { createJsonErrorRecoveryHook } from './json-error-recovery';
+export { createJsonErrorRecoveryHook } from './json-error-recovery/hook';
 export { createPhaseReminderHook } from './phase-reminder';
 export { createPostFileToolNudgeHook } from './post-file-tool-nudge';
 export { createTaskSessionManagerHook } from './task-session-manager';

+ 1 - 1
src/hooks/json-error-recovery/index.test.ts

@@ -5,7 +5,7 @@ import {
   JSON_ERROR_PATTERNS,
   JSON_ERROR_REMINDER,
   JSON_ERROR_TOOL_EXCLUDE_LIST,
-} from './index';
+} from './hook';
 
 describe('json-error-recovery hook', () => {
   let hook: ReturnType<typeof createJsonErrorRecoveryHook>;

+ 0 - 6
src/hooks/json-error-recovery/index.ts

@@ -1,6 +0,0 @@
-export {
-  createJsonErrorRecoveryHook,
-  JSON_ERROR_PATTERNS,
-  JSON_ERROR_REMINDER,
-  JSON_ERROR_TOOL_EXCLUDE_LIST,
-} from './hook';

+ 1 - 17
src/hooks/phase-reminder/index.ts

@@ -7,26 +7,10 @@
  */
 import { PHASE_REMINDER_TEXT } from '../../config/constants';
 import { SLIM_INTERNAL_INITIATOR_MARKER } from '../../utils';
+import type { MessageWithParts } from '../types';
 
 export const PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
 
-interface MessageInfo {
-  role: string;
-  agent?: string;
-  sessionID?: string;
-}
-
-interface MessagePart {
-  type: string;
-  text?: string;
-  [key: string]: unknown;
-}
-
-interface MessageWithParts {
-  info: MessageInfo;
-  parts: MessagePart[];
-}
-
 /**
  * Creates the experimental.chat.messages.transform hook for phase reminder injection.
  * This hook runs right before sending to API, so it doesn't affect UI display.

+ 2 - 4
src/hooks/post-file-tool-nudge/index.ts

@@ -5,8 +5,6 @@
 
 import { PHASE_REMINDER_TEXT } from '../../config/constants';
 
-const POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
-
 interface ToolExecuteAfterInput {
   tool: string;
   sessionID?: string;
@@ -31,7 +29,7 @@ export function createPostFileToolNudgeHook(
       return;
     }
 
-    if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
+    if (output.output.includes(PHASE_REMINDER_TEXT)) {
       return;
     }
 
@@ -39,7 +37,7 @@ export function createPostFileToolNudgeHook(
       output.output,
       '',
       '<internal_reminder>',
-      POST_FILE_TOOL_NUDGE,
+      PHASE_REMINDER_TEXT,
       '</internal_reminder>',
     ].join('\n');
   }

+ 7 - 25
src/hooks/task-session-manager/index.ts

@@ -11,7 +11,9 @@ import {
   parseTaskStatusOutput,
   SLIM_INTERNAL_INITIATOR_MARKER,
 } from '../../utils';
+import { isRecord as isObjectRecord } from '../../utils/guards';
 import { log } from '../../utils/logger';
+import type { MessagePart, MessageWithParts } from '../types';
 
 interface TaskArgs {
   description?: unknown;
@@ -48,22 +50,6 @@ interface PendingContextFile {
   lastReadAt: number;
 }
 
-interface ChatMessagePart {
-  type: string;
-  text?: string;
-  [key: string]: unknown;
-}
-
-interface ChatMessage {
-  info: {
-    role: string;
-    agent?: string;
-    sessionID?: string;
-    id?: string;
-  };
-  parts: ChatMessagePart[];
-}
-
 const BACKGROUND_JOB_BOARD_SENTINEL = 'SENTINEL: background-job-board-v2';
 const BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
 const BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
@@ -88,8 +74,8 @@ function djb2Hash(str: string): string {
  * Prefers part.id, then message.info.id + partIndex, then content-derived hash.
  */
 function createOccurrenceId(
-  part: ChatMessagePart,
-  message: ChatMessage,
+  part: MessagePart,
+  message: MessageWithParts,
   partIndex: number,
 ): string {
   // Prefer explicit part.id if available
@@ -126,10 +112,6 @@ function isAgentName(value: unknown): value is AgentName {
   return typeof value === 'string' && AGENT_NAME_SET.has(value as AgentName);
 }
 
-function isObjectRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === 'object' && value !== null;
-}
-
 function extractPath(output: string): string | undefined {
   return /<path>([^<]+)<\/path>/.exec(output)?.[1];
 }
@@ -314,8 +296,8 @@ export function createTaskSessionManagerHook(
   }
 
   function updateFromInjectedCompletion(
-    part: ChatMessagePart,
-    message: ChatMessage,
+    part: MessagePart,
+    message: MessageWithParts,
     _messageIndex: number,
     partIndex: number,
   ): BackgroundJobRecord | undefined {
@@ -668,7 +650,7 @@ export function createTaskSessionManagerHook(
 
     'experimental.chat.messages.transform': async (
       _input: Record<string, never>,
-      output: { messages: ChatMessage[] },
+      output: { messages: MessageWithParts[] },
     ): Promise<void> => {
       for (const [messageIndex, message] of output.messages.entries()) {
         if (message.info.role !== 'user') continue;

+ 26 - 0
src/hooks/types.ts

@@ -0,0 +1,26 @@
+/**
+ * Shared message type shapes for the OpenCode plugin API's `messages` array.
+ *
+ * These types describe the structure of chat messages passed through
+ * `experimental.chat.messages.transform` and related hooks. All fields
+ * are unioned across the files that previously defined them privately —
+ * optional extras are harmless under structural typing.
+ */
+
+export type MessageInfo = {
+  role: string;
+  agent?: string;
+  sessionID?: string;
+  id?: string;
+};
+
+export type MessagePart = {
+  type: string;
+  text?: string;
+  [key: string]: unknown;
+};
+
+export type MessageWithParts = {
+  info: MessageInfo;
+  parts: MessagePart[];
+};

+ 2 - 12
src/index.ts

@@ -30,6 +30,7 @@ import {
   ForegroundFallbackManager,
 } from './hooks';
 import { processImageAttachments } from './hooks/image-hook';
+import type { MessageWithParts } from './hooks/types';
 import { createInterviewManager } from './interview';
 import { createBuiltinMcps } from './mcp';
 import {
@@ -918,18 +919,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       input: Record<string, never>,
       output: { messages: unknown[] },
     ): Promise<void> => {
-      // Type assertion since we know the structure matches
-      // MessageWithParts[]
-      const typedOutput = output as {
-        messages: Array<{
-          info: { role: string; agent?: string; sessionID?: string };
-          parts: Array<{
-            type: string;
-            text?: string;
-            [key: string]: unknown;
-          }>;
-        }>;
-      };
+      const typedOutput = output as { messages: MessageWithParts[] };
 
       for (const message of typedOutput.messages) {
         if (message.info.role !== 'user') {

+ 1 - 4
src/tools/cancel-task.ts

@@ -4,6 +4,7 @@ import {
   tool,
 } from '@opencode-ai/plugin';
 import type { BackgroundJobBoard } from '../utils/background-job-board';
+import { isRecord as isObjectRecord } from '../utils/guards';
 import { log } from '../utils/logger';
 import { abortSessionWithTimeout, withTimeout } from '../utils/session';
 
@@ -460,10 +461,6 @@ function delay(ms: number): Promise<void> {
   return new Promise((resolve) => setTimeout(resolve, ms));
 }
 
-function isObjectRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === 'object' && value !== null;
-}
-
 function isSessionID(value: string): boolean {
   return /^ses_[\w-]+$/.test(value);
 }

+ 1 - 188
src/utils/agent-variant.test.ts

@@ -1,12 +1,6 @@
 import { describe, expect, test } from 'bun:test';
 import type { PluginConfig } from '../config';
-import {
-  applyAgentVariant,
-  normalizeAgentName,
-  resolveAgentVariant,
-  resolveRuntimeAgentName,
-  rewriteDisplayNameMentions,
-} from './agent-variant';
+import { normalizeAgentName, resolveRuntimeAgentName } from './agent-variant';
 
 describe('normalizeAgentName', () => {
   test('returns name unchanged if no @ prefix', () => {
@@ -30,89 +24,6 @@ describe('normalizeAgentName', () => {
   });
 });
 
-describe('resolveAgentVariant', () => {
-  test('returns undefined when config is undefined', () => {
-    expect(resolveAgentVariant(undefined, 'oracle')).toBeUndefined();
-  });
-
-  test('returns undefined when agents is undefined', () => {
-    const config = {} as PluginConfig;
-    expect(resolveAgentVariant(config, 'oracle')).toBeUndefined();
-  });
-
-  test('returns undefined when agent has no variant', () => {
-    const config = {
-      agents: {
-        oracle: { model: 'gpt-4' },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, 'oracle')).toBeUndefined();
-  });
-
-  test('returns variant when configured', () => {
-    const config = {
-      agents: {
-        oracle: { variant: 'high' },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, 'oracle')).toBe('high');
-  });
-
-  test('normalizes agent name with @ prefix', () => {
-    const config = {
-      agents: {
-        oracle: { variant: 'low' },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, '@oracle')).toBe('low');
-  });
-
-  test('returns undefined for empty string variant', () => {
-    const config = {
-      agents: {
-        oracle: { variant: '' },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, 'oracle')).toBeUndefined();
-  });
-
-  test('returns undefined for whitespace-only variant', () => {
-    const config = {
-      agents: {
-        oracle: { variant: '   ' },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, 'oracle')).toBeUndefined();
-  });
-
-  test('trims variant whitespace', () => {
-    const config = {
-      agents: {
-        oracle: { variant: '  medium  ' },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, 'oracle')).toBe('medium');
-  });
-
-  test('returns undefined for non-string variant', () => {
-    const config = {
-      agents: {
-        oracle: { variant: 123 as unknown as string },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, 'oracle')).toBeUndefined();
-  });
-
-  test('resolves displayName alias to internal agent for variant lookup', () => {
-    const config = {
-      agents: {
-        oracle: { displayName: 'advisor', variant: 'high' },
-      },
-    } as PluginConfig;
-    expect(resolveAgentVariant(config, '@advisor')).toBe('high');
-  });
-});
-
 describe('resolveRuntimeAgentName', () => {
   test('keeps internal agent names unchanged', () => {
     const config = {
@@ -164,101 +75,3 @@ describe('resolveRuntimeAgentName', () => {
     expect(resolveRuntimeAgentName(config, '  @unknown  ')).toBe('unknown');
   });
 });
-
-describe('rewriteDisplayNameMentions', () => {
-  test('rewrites displayName mentions to internal names for direct invocation', () => {
-    const config = {
-      agents: {
-        oracle: { displayName: 'advisor' },
-      },
-    } as PluginConfig;
-
-    expect(rewriteDisplayNameMentions(config, 'ask @advisor about this')).toBe(
-      'ask @oracle about this',
-    );
-  });
-
-  test('keeps internal mentions working while rewriting aliases', () => {
-    const config = {
-      agents: {
-        oracle: { displayName: 'advisor' },
-      },
-    } as PluginConfig;
-
-    expect(
-      rewriteDisplayNameMentions(config, 'compare @advisor with @oracle'),
-    ).toBe('compare @oracle with @oracle');
-  });
-
-  test('does not rewrite embedded text such as email addresses', () => {
-    const config = {
-      agents: {
-        oracle: { displayName: 'advisor' },
-      },
-    } as PluginConfig;
-
-    expect(
-      rewriteDisplayNameMentions(
-        config,
-        'email foo@advisor.com and ask @advisor directly',
-      ),
-    ).toBe('email foo@advisor.com and ask @oracle directly');
-  });
-
-  test('resolves custom agents by displayName for variant/runtime lookups', () => {
-    const config = {
-      agents: {
-        'custom-reviewer': {
-          displayName: 'reviewer',
-          variant: 'high',
-          model: 'openai/gpt-5.5',
-        },
-      },
-    } as PluginConfig;
-
-    expect(resolveRuntimeAgentName(config, '@reviewer')).toBe(
-      'custom-reviewer',
-    );
-    expect(
-      rewriteDisplayNameMentions(config, 'ask @reviewer for details'),
-    ).toBe('ask @custom-reviewer for details');
-    expect(resolveAgentVariant(config, '@reviewer')).toBe('high');
-  });
-});
-
-describe('applyAgentVariant', () => {
-  test('returns body unchanged when variant is undefined', () => {
-    const body = { agent: 'oracle', parts: [] };
-    const result = applyAgentVariant(undefined, body);
-    expect(result).toEqual(body);
-    expect(result).toBe(body); // Same reference
-  });
-
-  test('returns body unchanged when body already has variant', () => {
-    const body = { agent: 'oracle', variant: 'medium', parts: [] };
-    const result = applyAgentVariant('high', body);
-    expect(result.variant).toBe('medium');
-    expect(result).toBe(body); // Same reference
-  });
-
-  test('applies variant to body without variant', () => {
-    const body = { agent: 'oracle', parts: [] };
-    const result = applyAgentVariant('high', body);
-    expect(result.variant).toBe('high');
-    expect(result.agent).toBe('oracle');
-    expect(result).not.toBe(body); // New object
-  });
-
-  test('preserves all existing body properties', () => {
-    const body = {
-      agent: 'oracle',
-      parts: [{ type: 'text' as const, text: 'hello' }],
-      tools: { task: false },
-    };
-    const result = applyAgentVariant('low', body);
-    expect(result.agent).toBe('oracle');
-    expect(result.parts).toEqual([{ type: 'text', text: 'hello' }]);
-    expect(result.tools).toEqual({ task: false });
-    expect(result.variant).toBe('low');
-  });
-});

+ 0 - 76
src/utils/agent-variant.ts

@@ -4,7 +4,6 @@ import {
   getCustomAgentNames,
   type PluginConfig,
 } from '../config';
-import { log } from './logger';
 
 /**
  * Normalizes an agent name by trimming whitespace and removing the optional @ prefix.
@@ -29,42 +28,6 @@ function getRuntimeAgentNames(config?: PluginConfig): string[] {
   return [...unique];
 }
 
-/**
- * Resolves the variant configuration for a specific agent.
- *
- * Looks up the agent's variant in the plugin configuration. Returns undefined if:
- * - No config is provided
- * - The agent has no variant configured
- * - The variant is not a string
- * - The variant is empty or whitespace-only
- *
- * @param config - The plugin configuration object
- * @param agentName - The name of the agent (with or without @ prefix)
- * @returns The trimmed variant string, or undefined if no valid variant is found
- *
- * @example
- * resolveAgentVariant(config, "@oracle") // returns "high" if configured
- */
-export function resolveAgentVariant(
-  config: PluginConfig | undefined,
-  agentName: string,
-): string | undefined {
-  const normalized = resolveRuntimeAgentName(config, agentName);
-  const rawVariant = getAgentOverride(config, normalized)?.variant;
-
-  if (typeof rawVariant !== 'string') {
-    return undefined;
-  }
-
-  const trimmed = rawVariant.trim();
-  if (trimmed.length === 0) {
-    return undefined;
-  }
-
-  log(`[variant] resolved variant="${trimmed}" for agent "${normalized}"`);
-  return trimmed;
-}
-
 /**
  * Resolve a runtime-provided agent name to an internal agent name.
  *
@@ -151,42 +114,3 @@ export function createDisplayNameMentionRewriter(
     return rewritten;
   };
 }
-
-/**
- * Rewrites user-facing display-name mentions (e.g. @advisor) into internal
- * agent mentions (e.g. @oracle) for runtime routing.
- */
-export function rewriteDisplayNameMentions(
-  config: PluginConfig | undefined,
-  text: string,
-): string {
-  return createDisplayNameMentionRewriter(config)(text);
-}
-
-/**
- * Applies a variant to a request body if the body doesn't already have one.
- *
- * This function will NOT override an existing variant in the body. If no variant
- * is provided or the body already has a variant, the original body is returned.
- *
- * @template T - The type of the body object, must have an optional variant property
- * @param variant - The variant string to apply (or undefined)
- * @param body - The request body object
- * @returns The body with the variant applied (new object) or the original body unchanged
- *
- * @example
- * applyAgentVariant("high", { agent: "oracle" }) // returns { agent: "oracle", variant: "high" }
- * applyAgentVariant("high", { agent: "oracle", variant: "low" }) // returns original body with variant: "low"
- */
-export function applyAgentVariant<T extends { variant?: string }>(
-  variant: string | undefined,
-  body: T,
-): T {
-  if (!variant) {
-    return body;
-  }
-  if (body.variant) {
-    return body;
-  }
-  return { ...body, variant };
-}

+ 0 - 2
src/utils/compat.ts

@@ -2,8 +2,6 @@ import type { ChildProcess } from 'node:child_process';
 import { spawn as nodeSpawn } from 'node:child_process';
 import { writeFile as fsWriteFile } from 'node:fs/promises';
 
-export const isBun = typeof globalThis.Bun !== 'undefined';
-
 export interface CrossSpawnResult {
   proc: ChildProcess;
   /** Collects all stdout into a string */

+ 0 - 12
src/utils/env.ts

@@ -1,12 +0,0 @@
-export function getEnv(name: string): string | undefined {
-  const bunValue = (globalThis as { Bun?: { env?: Record<string, string> } })
-    .Bun?.env?.[name];
-  if (typeof bunValue === 'string' && bunValue.length > 0) return bunValue;
-
-  const processValue = (
-    globalThis as { process?: { env?: Record<string, string | undefined> } }
-  ).process?.env?.[name];
-  return typeof processValue === 'string' && processValue.length > 0
-    ? processValue
-    : undefined;
-}

+ 6 - 0
src/utils/guards.ts

@@ -0,0 +1,6 @@
+/**
+ * Shared type guard: checks if a value is a non-null object.
+ */
+export function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === 'object' && value !== null;
+}

+ 1 - 2
src/utils/index.ts

@@ -1,8 +1,7 @@
 export * from './agent-variant';
 export * from './background-job-board';
-export * from './env';
 export * from './internal-initiator';
-export { getLogDir, initLogger, log, resetLogger } from './logger';
+export { getLogDir, initLogger, log } from './logger';
 export * from './polling';
 export * from './session';
 export * from './task';

+ 2 - 4
src/utils/internal-initiator.ts

@@ -1,10 +1,8 @@
+import { isRecord } from './guards';
+
 export const SLIM_INTERNAL_INITIATOR_MARKER =
   '<!-- SLIM_INTERNAL_INITIATOR -->';
 
-function isRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === 'object' && value !== null;
-}
-
 export function createInternalAgentTextPart(text: string): {
   type: 'text';
   text: string;

+ 2 - 3
src/utils/logger.ts

@@ -76,6 +76,8 @@ export function initLogger(sessionId: string): void {
   cleanupOldLogs(dir);
 }
 
+export { getLogDir };
+
 /** @internal Reset logger state for testing */
 export function resetLogger(): void {
   logFile = null;
@@ -86,9 +88,6 @@ export function resetLogger(): void {
 export async function flushLoggerForTesting(): Promise<void> {
   await writeChain;
 }
-
-export { getLogDir };
-
 export function log(message: string, data?: unknown): void {
   const target = logFile;
   if (!target) return; // Uninitialized — silently no-op

+ 0 - 26
src/utils/task.ts

@@ -17,18 +17,6 @@ export interface TaskStatusOutput {
   result?: string;
 }
 
-export type TaskStatusClassification =
-  | 'running'
-  | 'terminal'
-  | 'timeout'
-  | 'transient_process_error'
-  | 'unknown_error';
-
-const TRANSIENT_PROCESS_ERROR_TEXT = new Set([
-  'Task is not running in this process and has no final output.',
-  'Task is not running in this process and has not produced output.',
-]);
-
 export function parseTaskIdFromTaskOutput(output: string): string | undefined {
   const xmlMatch = /<task\s+[^>]*\bid=["']([^"']+)["'][^>]*>/i.exec(output);
   if (xmlMatch) return xmlMatch[1];
@@ -80,20 +68,6 @@ export function parseTaskStatusOutput(
   };
 }
 
-export function classifyTaskStatusOutput(
-  status: TaskStatusOutput,
-): TaskStatusClassification {
-  if (status.timedOut) return 'timeout';
-  if (status.state === 'running') return 'running';
-  if (status.state === 'completed' || status.state === 'cancelled') {
-    return 'terminal';
-  }
-  if (TRANSIENT_PROCESS_ERROR_TEXT.has(status.result ?? '')) {
-    return 'transient_process_error';
-  }
-  return 'unknown_error';
-}
-
 export function parseTaskStateFromOutput(
   output: string,
 ): TaskOutputState | undefined {