← Back to task

Commit bf5c33c5

commit bf5c33c563939d6db1182d3bf5a99024dbdb2ea7
Author: Ben Sima <ben@bensima.com>
Date:   Thu Jan 1 09:17:30 2026

    Remove SubagentRole system, keep only Coder (t-314)
    
    Simplify the subagent system to only support Coder:
    - Remove SubagentRole variants: WebCrawler, CodeReviewer, DataExtractor, Researcher, General, CustomRole
    - Remove role-related functions: toolsForRole variants, modelForRole variants, roleDescription, systemPromptForRole, loadSystemPromptForRole
    - Remove runGenericSubagent (only Coder uses hardened Coder module)
    - Rename spawn_subagent tool to spawn_coder
    - Update tool description to mention skills for non-coding tasks
    - Remove unused imports (Provider, Prompts, WebSearch, WebReader, Tools, Python)
    
    Other roles have been migrated to skills (t-313):
    - web-research skill replaces WebCrawler
    - code-review skill replaces CodeReviewer
    - data-extraction skill replaces DataExtractor
    - research skill replaces Researcher
    
    ~320 lines removed.
    
    Task-Id: t-314

diff --git a/Omni/Agent/Subagent.hs b/Omni/Agent/Subagent.hs
index ed215a38..41b05be0 100644
--- a/Omni/Agent/Subagent.hs
+++ b/Omni/Agent/Subagent.hs
@@ -2,19 +2,21 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE NoImplicitPrelude #-}
 
--- | Subagent system for spawning specialized agents.
+-- | Subagent system for spawning the Coder agent.
 --
--- Enables the orchestrator (Ava) to delegate focused tasks to specialized
--- subagents that run with their own tool sets and resource limits.
+-- Enables the orchestrator (Ava) to delegate coding tasks to a specialized
+-- Coder subagent that runs with its own tool set and resource limits.
 --
 -- Key features:
--- - Role-based tool selection (WebCrawler, CodeReviewer, etc.)
+-- - Hardened Coder with init/verify/commit phases
 -- - Per-subagent resource limits (timeout, cost, tokens)
 -- - Structured result format with confidence scores
 -- - No sub-subagent spawning (hierarchical control)
 -- - Async execution with status polling
 -- - Audit logging for all events
 --
+-- For non-coding tasks (research, web crawling, etc.), use skills instead.
+--
 -- : out omni-agent-subagent
 -- : dep aeson
 -- : dep async
@@ -71,7 +73,6 @@ module Omni.Agent.Subagent
     SubagentApiKeys (..),
     toolsForRole,
     modelForRole,
-    systemPromptForRole,
 
     -- * Defaults
     defaultSubagentConfig,
@@ -96,14 +97,8 @@ import qualified Data.UUID
 import qualified Data.UUID.V4
 import qualified Omni.Agent.AuditLog as AuditLog
 import qualified Omni.Agent.Engine as Engine
-import qualified Omni.Agent.Prompts.Core as Prompts
-import qualified Omni.Agent.Provider as Provider
 import qualified Omni.Agent.Subagent.Coder as Coder
 import qualified Omni.Agent.Subagent.Jobs as Jobs
-import qualified Omni.Agent.Tools as Tools
-import qualified Omni.Agent.Tools.Python as Python
-import qualified Omni.Agent.Tools.WebReader as WebReader
-import qualified Omni.Agent.Tools.WebSearch as WebSearch
 import qualified Omni.Test as Test
 import System.IO.Unsafe (unsafePerformIO)
 import Text.Printf (printf)
@@ -205,13 +200,12 @@ test =
   Test.group
     "Omni.Agent.Subagent"
     [ Test.unit "SubagentRole JSON roundtrip" <| do
-        let roles = [WebCrawler, CodeReviewer, DataExtractor, Researcher]
-        forM_ roles <| \role ->
-          case Aeson.decode (Aeson.encode role) of
-            Nothing -> Test.assertFailure ("Failed to decode role: " <> show role)
-            Just decoded -> decoded Test.@=? role,
+        let role = Coder
+        case Aeson.decode (Aeson.encode role) of
+          Nothing -> Test.assertFailure "Failed to decode role"
+          Just decoded -> decoded Test.@=? role,
       Test.unit "SubagentConfig JSON roundtrip" <| do
-        let cfg = defaultSubagentConfig WebCrawler "test task"
+        let cfg = defaultSubagentConfig Coder "test task"
         case Aeson.decode (Aeson.encode cfg) of
           Nothing -> Test.assertFailure "Failed to decode SubagentConfig"
           Just decoded -> subagentTask decoded Test.@=? "test task",
@@ -241,24 +235,15 @@ test =
           case Aeson.decode (Aeson.encode status) of
             Nothing -> Test.assertFailure ("Failed to decode status: " <> show status)
             Just decoded -> decoded Test.@=? status,
-      Test.unit "toolsForRole WebCrawler has web tools" <| do
+      Test.unit "toolsForRole Coder has coder tools" <| do
         let keys = SubagentApiKeys "test-openrouter-key" (Just "test-kagi-key")
-        let tools = toolsForRole WebCrawler keys
-        let names = map Engine.toolName tools
-        ("web_search" `elem` names) Test.@=? True
-        ("read_webpages" `elem` names) Test.@=? True,
-      Test.unit "toolsForRole CodeReviewer has code tools" <| do
-        let keys = SubagentApiKeys "test-openrouter-key" Nothing
-        let tools = toolsForRole CodeReviewer keys
-        let names = map Engine.toolName tools
-        ("read_file" `elem` names) Test.@=? True
-        ("search_codebase" `elem` names) Test.@=? True,
-      Test.unit "modelForRole returns appropriate models" <| do
-        modelForRole WebCrawler Test.@=? "anthropic/claude-3-haiku"
-        modelForRole CodeReviewer Test.@=? "anthropic/claude-sonnet-4"
-        modelForRole Researcher Test.@=? "anthropic/claude-sonnet-4",
+        let tools = toolsForRole Coder keys
+        -- Coder tools come from Coder module
+        (length tools > 0) Test.@=? True,
+      Test.unit "modelForRole returns appropriate model" <| do
+        modelForRole Coder Test.@=? "anthropic/claude-sonnet-4",
       Test.unit "defaultSubagentConfig has sensible defaults" <| do
-        let cfg = defaultSubagentConfig WebCrawler "task"
+        let cfg = defaultSubagentConfig Coder "task"
         subagentTimeout cfg Test.@=? 600
         subagentMaxCost cfg Test.@=? 100.0
         subagentMaxTokens cfg Test.@=? 200000
@@ -266,11 +251,11 @@ test =
       Test.unit "spawnSubagentTool has correct name" <| do
         let keys = SubagentApiKeys "test-openrouter-key" (Just "test-kagi-key")
         let tool = spawnSubagentTool keys
-        Engine.toolName tool Test.@=? "spawn_subagent",
-      Test.unit "spawn_subagent returns approval request when not confirmed" <| do
+        Engine.toolName tool Test.@=? "spawn_coder",
+      Test.unit "spawn_coder returns approval request when not confirmed" <| do
         let keys = SubagentApiKeys "test-openrouter-key" (Just "test-kagi-key")
         let tool = spawnSubagentTool keys
-        let args = Aeson.object ["role" .= ("web_crawler" :: Text), "task" .= ("test task" :: Text)]
+        let args = Aeson.object ["task" .= ("test task" :: Text), "namespace" .= ("Omni/Test" :: Text)]
         result <- Engine.toolExecute tool args
         case result of
           Aeson.Object obj -> do
@@ -278,7 +263,7 @@ test =
             status Test.@=? Just (Aeson.String "awaiting_approval")
           _ -> Test.assertFailure "Expected object response",
       Test.unit "pending spawn create and lookup works" <| do
-        let config = defaultSubagentConfig WebCrawler "test pending task"
+        let config = defaultSubagentConfig Coder "test pending task"
         (pid, sid) <- createPendingSpawn config 12345 Nothing
         when (Text.null pid) <| Test.assertFailure "pending ID should not be empty"
         when (Text.null sid) <| Test.assertFailure "subagent ID should not be empty"
@@ -293,7 +278,7 @@ test =
         afterRemove <- getPendingSpawn pid
         afterRemove Test.@=? Nothing,
       Test.unit "pending spawn registry is isolated" <| do
-        let config = defaultSubagentConfig Researcher "isolated test"
+        let config = defaultSubagentConfig Coder "isolated test"
         (pid1, _) <- createPendingSpawn config 111 Nothing
         (pid2, _) <- createPendingSpawn config 222 Nothing
         when (pid1 == pid2) <| Test.assertFailure "IDs should be different"
@@ -304,35 +289,20 @@ test =
         removePendingSpawn pid2
     ]
 
+-- | Subagent role - currently only Coder is supported.
+-- Other roles (web_crawler, researcher, etc.) have been migrated to skills.
 data SubagentRole
-  = WebCrawler
-  | CodeReviewer
-  | DataExtractor
-  | Researcher
-  | Coder
-  | General
-  | CustomRole Text
+  = Coder
   deriving (Show, Eq, Generic)
 
 instance Aeson.ToJSON SubagentRole where
-  toJSON WebCrawler = Aeson.String "web_crawler"
-  toJSON CodeReviewer = Aeson.String "code_reviewer"
-  toJSON DataExtractor = Aeson.String "data_extractor"
-  toJSON Researcher = Aeson.String "researcher"
   toJSON Coder = Aeson.String "coder"
-  toJSON General = Aeson.String "general"
-  toJSON (CustomRole name) = Aeson.String name
 
 instance Aeson.FromJSON SubagentRole where
   parseJSON = Aeson.withText "SubagentRole" parseRole
     where
-      parseRole "web_crawler" = pure WebCrawler
-      parseRole "code_reviewer" = pure CodeReviewer
-      parseRole "data_extractor" = pure DataExtractor
-      parseRole "researcher" = pure Researcher
       parseRole "coder" = pure Coder
-      parseRole "general" = pure General
-      parseRole name = pure (CustomRole name)
+      parseRole _ = empty
 
 -- | Per-spawn guardrails that override engine defaults
 data SpawnGuardrails = SpawnGuardrails
@@ -699,14 +669,9 @@ defaultSubagentConfig role task =
       subagentWorkDir = Nothing
     }
 
+-- | Model for a subagent role
 modelForRole :: SubagentRole -> Text
-modelForRole WebCrawler = "anthropic/claude-3-haiku"
-modelForRole CodeReviewer = "anthropic/claude-sonnet-4"
-modelForRole DataExtractor = "anthropic/claude-3-haiku"
-modelForRole Researcher = "anthropic/claude-sonnet-4"
 modelForRole Coder = "anthropic/claude-sonnet-4"
-modelForRole General = "anthropic/claude-sonnet-4"
-modelForRole (CustomRole _) = "anthropic/claude-sonnet-4"
 
 data SubagentApiKeys = SubagentApiKeys
   { subagentOpenRouterKey :: Text,
@@ -714,122 +679,20 @@ data SubagentApiKeys = SubagentApiKeys
   }
   deriving (Show, Eq)
 
+-- | Tools for a subagent role
+-- Coder uses the hardened Coder module with init/verify/commit phases
 toolsForRole :: SubagentRole -> SubagentApiKeys -> [Engine.Tool]
-toolsForRole WebCrawler keys =
-  let webSearchTools = case subagentKagiKey keys of
-        Just kagiKey -> [WebSearch.webSearchTool kagiKey]
-        Nothing -> []
-   in webSearchTools
-        <> [ WebReader.webReaderTool (subagentOpenRouterKey keys),
-             Tools.searchCodebaseTool
-           ]
-toolsForRole CodeReviewer _keys =
-  [ Tools.readFileTool,
-    Tools.searchCodebaseTool,
-    Tools.searchAndReadTool,
-    Tools.runBashTool
-  ]
-toolsForRole DataExtractor keys =
-  [ WebReader.webReaderTool (subagentOpenRouterKey keys),
-    Tools.readFileTool,
-    Tools.searchCodebaseTool
-  ]
-toolsForRole Researcher keys =
-  let webSearchTools = case subagentKagiKey keys of
-        Just kagiKey -> [WebSearch.webSearchTool kagiKey]
-        Nothing -> []
-   in webSearchTools
-        <> [ WebReader.webReaderTool (subagentOpenRouterKey keys),
-             Tools.readFileTool,
-             Tools.searchCodebaseTool,
-             Tools.searchAndReadTool
-           ]
--- Coder uses the hardened Coder module, toolsForRole not used
 toolsForRole Coder _keys = Coder.coderTools
--- General role: balanced tools for non-specialized tasks
-toolsForRole General _keys =
-  [ Tools.readFileTool,
-    Tools.writeFileTool,
-    Tools.editFileTool,
-    Tools.runBashTool,
-    Python.pythonExecTool,
-    Tools.searchCodebaseTool,
-    Tools.searchAndReadTool
-  ]
-toolsForRole (CustomRole _) keys = toolsForRole Researcher keys
-
--- | Load system prompt from template (required)
-loadSystemPromptForRole :: SubagentRole -> Text -> Maybe Text -> IO Text
-loadSystemPromptForRole role task maybeContext = do
-  let ctx =
-        Aeson.object
-          [ "role_description" .= roleDescription role,
-            "task" .= task,
-            "context" .= maybeContext
-          ]
-  result <- Prompts.renderPrompt "subagents/generic/system" ctx
-  case result of
-    Right prompt -> pure prompt
-    Left err -> panic <| "Failed to load subagent system prompt: " <> err
-
--- | Legacy fallback prompt (deprecated - use template instead)
-systemPromptForRole :: SubagentRole -> Text -> Maybe Text -> Text
-systemPromptForRole role task maybeContext =
-  Text.unlines
-    [ "You are a specialized " <> roleDescription role <> " subagent working on a focused task.",
-      "",
-      "## Your Task",
-      task,
-      "",
-      maybe "" (\ctx -> "## Context from Orchestrator\n" <> ctx <> "\n") maybeContext,
-      "## Guidelines",
-      "1. Be EFFICIENT with context - extract only key facts, don't save full page contents",
-      "2. Summarize findings as you go rather than accumulating raw data",
-      "3. Limit web page reads to 3-5 most relevant sources",
-      "4. Work iteratively: search → skim results → read best 2-3 → synthesize",
-      "5. ALWAYS cite sources - every claim needs a URL",
-      "6. Stop when you have sufficient information - don't over-research",
-      "",
-      "## Output Format",
-      "Return findings as a list of structured insights:",
-      "",
-      "```json",
-      "{",
-      "  \"summary\": \"Brief overall summary (1-2 sentences)\",",
-      "  \"confidence\": 0.85,",
-      "  \"findings\": [",
-      "    {",
-      "      \"claim\": \"The key insight or fact discovered\",",
-      "      \"source_url\": \"https://example.com/page\",",
-      "      \"quote\": \"Relevant excerpt supporting the claim\",",
-      "      \"source_name\": \"Example Site\"",
-      "    }",
-      "  ],",
-      "  \"caveats\": \"Any limitations or uncertainties\"",
-      "}",
-      "```"
-    ]
 
-roleDescription :: SubagentRole -> Text
-roleDescription WebCrawler = "web research"
-roleDescription CodeReviewer = "code review"
-roleDescription DataExtractor = "data extraction"
-roleDescription Researcher = "research"
-roleDescription Coder = "coding"
-roleDescription General = "general-purpose"
-roleDescription (CustomRole name) = name
+
 
 runSubagent :: SubagentApiKeys -> SubagentConfig -> IO SubagentResult
 runSubagent keys config = runSubagentWithCallbacks keys config defaultCallbacks
 
 runSubagentWithCallbacks :: SubagentApiKeys -> SubagentConfig -> SubagentCallbacks -> IO SubagentResult
-runSubagentWithCallbacks keys config callbacks = do
-  let role = subagentRole config
-
+runSubagentWithCallbacks keys config callbacks =
   -- Coder role uses the hardened Coder module with init/verify/commit phases
-  case role of
-    Coder -> runCoderSubagentWrapper keys config callbacks
-    _ -> runGenericSubagent keys config callbacks
+  runCoderSubagentWrapper keys config callbacks
 
 -- | Run Coder subagent using the hardened Coder module
 runCoderSubagentWrapper :: SubagentApiKeys -> SubagentConfig -> SubagentCallbacks -> IO SubagentResult
@@ -954,134 +817,25 @@ runCoderSubagentWrapper keys config callbacks = do
           onSubagentComplete callbacks finalResult
           pure finalResult
 
--- | Run generic (non-Coder) subagent
-runGenericSubagent :: SubagentApiKeys -> SubagentConfig -> SubagentCallbacks -> IO SubagentResult
-runGenericSubagent keys config callbacks = do
-  startTime <- Clock.getCurrentTime
-
-  let role = subagentRole config
-  let model = fromMaybe (modelForRole role) (subagentModel config)
-  let tools = toolsForRole role keys
-  systemPrompt <- loadSystemPromptForRole role (subagentTask config) (subagentContext config)
-
-  onSubagentStart callbacks ("Starting " <> tshow role <> " subagent...")
-
-  let provider = Provider.defaultOpenRouter (subagentOpenRouterKey keys) model
-
-  let guardrails =
-        Engine.Guardrails
-          { Engine.guardrailMaxCostCents = subagentMaxCost config,
-            Engine.guardrailMaxTokens = subagentMaxTokens config,
-            Engine.guardrailMaxDuplicateToolCalls = 20,
-            Engine.guardrailMaxTestFailures = 3,
-            Engine.guardrailMaxEditFailures = 5
-          }
-
-  let agentConfig =
-        Engine.AgentConfig
-          { Engine.agentModel = model,
-            Engine.agentTools = tools,
-            Engine.agentSystemPrompt = systemPrompt,
-            Engine.agentMaxIterations = subagentMaxIterations config,
-            Engine.agentGuardrails = guardrails
-          }
-
-  let engineConfig =
-        Engine.EngineConfig
-          { Engine.engineLLM = Engine.defaultLLM,
-            Engine.engineOnCost = \_ _ -> pure (),
-            Engine.engineOnActivity = onSubagentActivity callbacks,
-            Engine.engineOnToolCall = onSubagentToolCall callbacks,
-            Engine.engineOnAssistant = \_ -> pure (),
-            Engine.engineOnToolResult = \_ _ _ -> pure (),
-            Engine.engineOnComplete = pure (),
-            Engine.engineOnError = \_ -> pure (),
-            Engine.engineOnGuardrail = \_ -> pure (),
-            Engine.engineOnToolTrace = \_ _ _ _ -> pure Nothing
-          }
-
-  let timeoutMicros = subagentTimeout config * 1000000
-
-  resultOrTimeout <-
-    race
-      (threadDelay timeoutMicros)
-      (Engine.runAgentWithProvider engineConfig provider agentConfig (subagentTask config))
-
-  endTime <- Clock.getCurrentTime
-  let durationSecs = round (Clock.diffUTCTime endTime startTime)
-
-  let result = case resultOrTimeout of
-        Left () ->
-          SubagentResult
-            { subagentOutput = Aeson.object ["error" .= ("Timeout after " <> tshow (subagentTimeout config) <> " seconds" :: Text)],
-              subagentSummary = "Subagent timed out",
-              subagentConfidence = 0.0,
-              subagentTokensUsed = 0,
-              subagentCostCents = 0.0,
-              subagentDuration = durationSecs,
-              subagentIterations = 0,
-              subagentStatus = SubagentTimeout
-            }
-        Right (Left err) ->
-          let status = if "cost" `Text.isInfixOf` Text.toLower err then SubagentCostExceeded else SubagentError err
-           in SubagentResult
-                { subagentOutput = Aeson.object ["error" .= err],
-                  subagentSummary = "Subagent failed: " <> err,
-                  subagentConfidence = 0.0,
-                  subagentTokensUsed = 0,
-                  subagentCostCents = 0.0,
-                  subagentDuration = durationSecs,
-                  subagentIterations = 0,
-                  subagentStatus = status
-                }
-        Right (Right agentResult) ->
-          SubagentResult
-            { subagentOutput = Aeson.object ["response" .= Engine.resultFinalMessage agentResult],
-              subagentSummary = truncateSummary (Engine.resultFinalMessage agentResult),
-              subagentConfidence = 0.8,
-              subagentTokensUsed = Engine.resultTotalTokens agentResult,
-              subagentCostCents = Engine.resultTotalCost agentResult,
-              subagentDuration = durationSecs,
-              subagentIterations = Engine.resultIterations agentResult,
-              subagentStatus = SubagentSuccess
-            }
-
-  onSubagentComplete callbacks result
-  pure result
-  where
-    truncateSummary :: Text -> Text
-    truncateSummary txt =
-      let firstLine = Text.takeWhile (/= '\n') txt
-       in if Text.length firstLine > 200
-            then Text.take 197 firstLine <> "..."
-            else firstLine
-
+-- | Tool to spawn a coder subagent for code changes.
+-- For other workflows (research, web crawling, etc.), use skills instead.
 spawnSubagentTool :: SubagentApiKeys -> Engine.Tool
 spawnSubagentTool keys =
   Engine.Tool
-    { Engine.toolName = "spawn_subagent",
+    { Engine.toolName = "spawn_coder",
       Engine.toolDescription =
-        "Spawn a specialized subagent for a focused task. "
+        "Spawn a Coder subagent for code changes. The Coder runs in a hardened loop with "
+          <> "init (understand codebase) → implement → verify (build/test) → commit phases. "
           <> "IMPORTANT: First call with confirmed=false to get approval request, "
           <> "then present the approval to the user. Only call with confirmed=true "
           <> "after the user explicitly approves. "
-          <> "Available roles: web_crawler (fast web research), code_reviewer (thorough code analysis), "
-          <> "data_extractor (structured data extraction), researcher (general research), "
-          <> "coder (hardened coding with init/verify/commit - requires namespace and context), "
-          <> "general (balanced tools for non-specialized tasks), "
-          <> "custom (use custom_role_name and specify tools).",
+          <> "For non-coding tasks (research, data extraction, etc.), use skills instead.",
       Engine.toolJsonSchema =
         Aeson.object
           [ "type" .= ("object" :: Text),
             "properties"
               .= Aeson.object
-                [ "role"
-                    .= Aeson.object
-                      [ "type" .= ("string" :: Text),
-                        "enum" .= (["web_crawler", "code_reviewer", "data_extractor", "researcher", "coder", "general", "custom"] :: [Text]),
-                        "description" .= ("Subagent role determining tools and model" :: Text)
-                      ],
-                  "task"
+                [ "task"
                     .= Aeson.object
                       [ "type" .= ("string" :: Text),
                         "description" .= ("The specific task for the subagent to accomplish" :: Text)
@@ -1101,32 +855,11 @@ spawnSubagentTool keys =
                       [ "type" .= ("integer" :: Text),
                         "description" .= ("Timeout in seconds (default: 600)" :: Text)
                       ],
-                  "max_cost_cents"
-                    .= Aeson.object
-                      [ "type" .= ("number" :: Text),
-                        "description" .= ("Maximum cost in cents (default: 100)" :: Text)
-                      ],
                   "namespace"
                     .= Aeson.object
                       [ "type" .= ("string" :: Text),
                         "description" .= ("Code namespace like 'Omni/Agent/Subagent' (required for coder role)" :: Text)
                       ],
-                  "custom_role_name"
-                    .= Aeson.object
-                      [ "type" .= ("string" :: Text),
-                        "description" .= ("Name for custom role (when role=custom)" :: Text)
-                      ],
-                  "tools"
-                    .= Aeson.object
-                      [ "type" .= ("array" :: Text),
-                        "items" .= Aeson.object ["type" .= ("string" :: Text)],
-                        "description" .= ("Override default tools with specific tool names" :: Text)
-                      ],
-                  "system_prompt"
-                    .= Aeson.object
-                      [ "type" .= ("string" :: Text),
-                        "description" .= ("Additional system prompt instructions for this subagent" :: Text)
-                      ],
                   "guardrails"
                     .= Aeson.object
                       [ "type" .= ("object" :: Text),
@@ -1186,13 +919,7 @@ formatApprovalRequest config =
         <> costStr
         <> "\n\nProceed? (yes/no)"
     roleText = case subagentRole config of
-      WebCrawler -> "WebCrawler"
-      CodeReviewer -> "CodeReviewer"
-      DataExtractor -> "DataExtractor"
-      Researcher -> "Researcher"
       Coder -> "Coder"
-      General -> "General"
-      CustomRole name -> name
     estimatedTime :: Int
     estimatedTime = subagentTimeout config `div` 60
     costStr = Text.pack (printf "%.2f" (subagentMaxCost config / 100))
@@ -1302,76 +1029,37 @@ subagentTools keys = [spawnSubagentTool keys, checkSubagentTool]
 type ApprovalCallback = Int -> Text -> Text -> Text -> Int -> Double -> IO ()
 
 -- | Spawn subagent tool that requires external approval via callback
+-- | Coder spawning tool that requires Telegram button approval
 spawnSubagentToolWithApproval :: SubagentApiKeys -> Int -> Maybe Int -> ApprovalCallback -> Engine.Tool
 spawnSubagentToolWithApproval keys chatId threadId onApprovalNeeded =
   Engine.Tool
-    { Engine.toolName = "spawn_subagent",
+    { Engine.toolName = "spawn_coder",
       Engine.toolDescription =
-        "Request to spawn a specialized subagent for a focused task. "
+        "Request to spawn a Coder subagent for code changes. "
+          <> "The Coder runs in a hardened loop with init → implement → verify → commit phases. "
           <> "The user will receive a confirmation button to approve. "
-          <> "IMPORTANT: The subagent does NOT start until the user clicks Approve - "
+          <> "IMPORTANT: The coder does NOT start until the user clicks Approve - "
           <> "do NOT say 'spawned' or 'started', say 'requested' or 'awaiting approval'. "
-          <> "Available roles: web_crawler (fast web research), code_reviewer (thorough code analysis), "
-          <> "data_extractor (structured data extraction), researcher (general research), "
-          <> "coder (hardened coding with init/verify/commit - requires namespace and context), "
-          <> "general (balanced tools for non-specialized tasks), "
-          <> "custom (use custom_role_name and specify tools).",
+          <> "For non-coding tasks (research, data extraction, etc.), use skills instead.",
       Engine.toolJsonSchema =
         Aeson.object
           [ "type" .= ("object" :: Text),
             "properties"
               .= Aeson.object
-                [ "role"
-                    .= Aeson.object
-                      [ "type" .= ("string" :: Text),
-                        "enum" .= (["web_crawler", "code_reviewer", "data_extractor", "researcher", "coder", "general", "custom"] :: [Text]),
-                        "description" .= ("Subagent role determining tools and model" :: Text)
-                      ],
-                  "task"
+                [ "task"
                     .= Aeson.object
                       [ "type" .= ("string" :: Text),
-                        "description" .= ("The specific task for the subagent to accomplish" :: Text)
+                        "description" .= ("The specific coding task to accomplish" :: Text)
                       ],
                   "context"
                     .= Aeson.object
                       [ "type" .= ("string" :: Text),
-                        "description" .= ("Background context, related files, design decisions (required for coder)" :: Text)
-                      ],
-                  "model"
-                    .= Aeson.object
-                      [ "type" .= ("string" :: Text),
-                        "description" .= ("Override the default model for this role" :: Text)
-                      ],
-                  "timeout"
-                    .= Aeson.object
-                      [ "type" .= ("integer" :: Text),
-                        "description" .= ("Timeout in seconds (default: 600)" :: Text)
-                      ],
-                  "max_cost_cents"
-                    .= Aeson.object
-                      [ "type" .= ("number" :: Text),
-                        "description" .= ("Maximum cost in cents (default: 100)" :: Text)
+                        "description" .= ("Background context, related files, design decisions - REQUIRED" :: Text)
                       ],
                   "namespace"
                     .= Aeson.object
                       [ "type" .= ("string" :: Text),
-                        "description" .= ("Code namespace like 'Omni/Agent/Subagent' (required for coder role)" :: Text)
-                      ],
-                  "custom_role_name"
-                    .= Aeson.object
-                      [ "type" .= ("string" :: Text),
-                        "description" .= ("Name for custom role (when role=custom)" :: Text)
-                      ],
-                  "tools"
-                    .= Aeson.object
-                      [ "type" .= ("array" :: Text),
-                        "items" .= Aeson.object ["type" .= ("string" :: Text)],
-                        "description" .= ("Override default tools with specific tool names" :: Text)
-                      ],
-                  "system_prompt"
-                    .= Aeson.object
-                      [ "type" .= ("string" :: Text),
-                        "description" .= ("Additional system prompt instructions for this subagent" :: Text)
+                        "description" .= ("Code namespace like 'Omni/Agent/Subagent' - REQUIRED" :: Text)
                       ],
                   "guardrails"
                     .= Aeson.object
@@ -1398,13 +1086,7 @@ executeSpawnWithApproval _keys chatId threadId onApprovalNeeded v =
     Aeson.Success config -> do
       (pid, subagentId) <- createPendingSpawn config chatId threadId
       let roleText = case subagentRole config of
-            WebCrawler -> "web_crawler"
-            CodeReviewer -> "code_reviewer"
-            DataExtractor -> "data_extractor"
-            Researcher -> "researcher"
             Coder -> "coder"
-            General -> "general"
-            CustomRole name -> name
           estimatedMins = subagentTimeout config `div` 60
           maxCost = subagentMaxCost config
       onApprovalNeeded chatId pid roleText (subagentTask config) estimatedMins maxCost