← Back to task

Commit 4a4a4788

commit 4a4a4788ac919417310ee06c69c718e71048ffb0
Author: Ben Sima <ben@bensima.com>
Date:   Fri Jan 2 06:51:23 2026

    Add Omni/Ide/Coder.hs - Native Haskell coder agent
    
    Replaces pi-code.sh with a Haskell module using the Agent API:
    - runCoder :: CoderConfig -> IO CoderResult
    - Claims task, builds prompt with context
    - Runs agent with tools
    - Verifies compilation for buildable files
    
    Task-Id: t-278.2

diff --git a/Omni/Ide/Coder.hs b/Omni/Ide/Coder.hs
new file mode 100644
index 00000000..aba764a4
--- /dev/null
+++ b/Omni/Ide/Coder.hs
@@ -0,0 +1,336 @@
+#!/usr/bin/env run.sh
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Coder agent - runs coding tasks using the Agent API.
+--
+-- Replaces pi-code.sh with a native Haskell implementation.
+--
+-- Usage:
+--   runCoder taskId maybeExtraInstructions
+--
+-- What it does:
+--   1. Claims the task (sets status to in-progress)
+--   2. Builds prompt with task context (title, description, namespace, feedback)
+--   3. Runs agent to make code changes
+--   4. Verifies compilation if namespace is a buildable file
+--
+-- : out coder
+-- : dep aeson
+-- : dep process
+module Omni.Ide.Coder
+  ( runCoder,
+    CoderConfig (..),
+    CoderResult (..),
+    defaultCoderConfig,
+    main,
+    test,
+  )
+where
+
+import Alpha
+import qualified Data.Text as Text
+import qualified Data.Text.IO as TextIO
+import qualified Omni.Agent.Auth as Auth
+import qualified Omni.Agent.Engine as Engine
+import qualified Omni.Agent.Provider as Provider
+import qualified Omni.Agent.Tools as Tools
+import qualified Omni.Agent.Tools.WebSearch as WebSearch
+import qualified Omni.Task.Core as Task
+import qualified Omni.Test as Test
+import qualified System.Directory as Directory
+import qualified System.Environment as Environment
+import qualified System.Exit as Exit
+import qualified System.IO as IO
+import qualified System.Process as Process
+
+main :: IO ()
+main = Test.run test
+
+test :: Test.Tree
+test =
+  Test.group
+    "Omni.Ide.Coder"
+    [ Test.unit "defaultCoderConfig has sensible defaults" <| do
+        let cfg = defaultCoderConfig
+        coderMaxIterations cfg Test.@=? 50
+        coderMaxCostCents cfg Test.@=? 200
+    ]
+
+-- | Configuration for the coder agent
+data CoderConfig = CoderConfig
+  { coderTaskId :: Text,
+    coderExtraInstructions :: Maybe Text,
+    coderMaxIterations :: Int,
+    coderMaxCostCents :: Int,
+    coderVerbose :: Bool,
+    coderOnActivity :: Text -> IO (),
+    coderOnOutput :: Text -> IO ()
+  }
+
+defaultCoderConfig :: CoderConfig
+defaultCoderConfig =
+  CoderConfig
+    { coderTaskId = "",
+      coderExtraInstructions = Nothing,
+      coderMaxIterations = 50,
+      coderMaxCostCents = 200,
+      coderVerbose = True,
+      coderOnActivity = \msg -> TextIO.hPutStrLn IO.stderr <| "[coder] " <> msg,
+      coderOnOutput = TextIO.putStrLn
+    }
+
+-- | Result of running the coder
+data CoderResult
+  = CoderSuccess Text -- ^ Final message from agent
+  | CoderError Text -- ^ Error message
+  | CoderCompilationFailed Text -- ^ Code changes made but compilation failed
+  deriving (Show, Eq)
+
+-- | Run the coder agent on a task
+runCoder :: CoderConfig -> IO CoderResult
+runCoder cfg = do
+  let taskId = coderTaskId cfg
+      onActivity = coderOnActivity cfg
+
+  -- Claim task
+  onActivity <| "Claiming task " <> taskId <> "..."
+  allTasks <- Task.loadTasks
+  case Task.findTask taskId allTasks of
+    Nothing -> pure <| CoderError <| "Task not found: " <> taskId
+    Just task -> do
+      -- Update status to in-progress
+      Task.updateTaskStatus taskId Task.InProgress []
+
+      -- Build the prompt
+      let prompt = buildCoderPrompt task (coderExtraInstructions cfg)
+
+      onActivity <| "Task: " <> Task.taskTitle task
+      case Task.taskNamespace task of
+        Just ns -> onActivity <| "Namespace: " <> ns
+        Nothing -> pure ()
+
+      -- Run the agent
+      onActivity "Starting coder agent..."
+      result <- runCoderAgent cfg prompt
+
+      case result of
+        Left err -> pure <| CoderError err
+        Right response -> do
+          -- Post-coder verification
+          verifyResult <- verifyCompilation cfg task
+          case verifyResult of
+            Left err -> pure <| CoderCompilationFailed err
+            Right () -> pure <| CoderSuccess response
+
+-- | Build the prompt for the coder agent
+buildCoderPrompt :: Task.Task -> Maybe Text -> Text
+buildCoderPrompt task mExtraInstructions =
+  let taskId = Task.taskId task
+      title = Task.taskTitle task
+      desc = Task.taskDescription task
+      mNamespace = Task.taskNamespace task
+      comments = Task.taskComments task
+
+      basePrompt =
+        Text.unlines
+          [ "You are working on task " <> taskId <> ": " <> title,
+            "",
+            "## Description",
+            desc
+          ]
+
+      namespaceSection = case mNamespace of
+        Just ns ->
+          Text.unlines
+            [ "",
+              "## Namespace",
+              "Focus on: " <> ns
+            ]
+        Nothing -> ""
+
+      commentsSection =
+        if null comments
+          then ""
+          else
+            Text.unlines
+              [ "",
+                "## Previous Feedback",
+                Text.unlines <| map formatComment comments
+              ]
+
+      extraSection = case mExtraInstructions of
+        Just extra ->
+          Text.unlines
+            [ "",
+              "## Additional Instructions",
+              extra
+            ]
+        Nothing -> ""
+
+      guidelines = case mNamespace of
+        Just ns ->
+          Text.unlines
+            [ "",
+              "## Guidelines",
+              "- Make the necessary code changes",
+              "- You MUST run `bild " <> ns <> "` to verify compilation before finishing",
+              "- Fix any compilation errors before declaring the task complete",
+              "- Do NOT commit - a reviewer will handle that"
+            ]
+        Nothing ->
+          Text.unlines
+            [ "",
+              "## Guidelines",
+              "- Make the necessary changes",
+              "- Do NOT commit - a reviewer will handle that"
+            ]
+   in basePrompt <> namespaceSection <> commentsSection <> extraSection <> guidelines
+  where
+    formatComment :: Task.Comment -> Text
+    formatComment c =
+      "[" <> tshow (Task.commentCreatedAt c) <> "] " <> tshow (Task.commentAuthor c) <> ": " <> Task.commentText c
+
+-- | Run the agent with the given prompt
+runCoderAgent :: CoderConfig -> Text -> IO (Either Text Text)
+runCoderAgent cfg prompt = do
+  -- Resolve provider (prefer claude-code for cost efficiency)
+  provider <- resolveCoderProvider
+
+  case provider of
+    Left err -> pure <| Left err
+    Right prov -> do
+      -- Get web search tool if available
+      mKagiKey <- Environment.lookupEnv "KAGI_API_KEY"
+      let webSearchTools = case mKagiKey of
+            Just key -> [WebSearch.webSearchTool (Text.pack key)]
+            Nothing -> []
+          tools = Tools.allTools <> webSearchTools
+
+      -- Build agent config
+      let agentConfig =
+            Engine.defaultAgentConfig
+              { Engine.agentSystemPrompt = coderSystemPrompt,
+                Engine.agentTools = tools,
+                Engine.agentMaxIterations = coderMaxIterations cfg,
+                Engine.agentGuardrails =
+                  Engine.defaultGuardrails
+                    { Engine.guardrailMaxCostCents = fromIntegral (coderMaxCostCents cfg)
+                    }
+              }
+
+          engineConfig =
+            Engine.defaultEngineConfig
+              { Engine.engineOnActivity =
+                  if coderVerbose cfg
+                    then coderOnActivity cfg
+                    else \_ -> pure (),
+                Engine.engineOnToolCall =
+                  if coderVerbose cfg
+                    then \name args -> coderOnActivity cfg <| "Tool: " <> name <> " " <> Text.take 80 args
+                    else \_ _ -> pure ()
+              }
+
+      -- Run agent
+      result <- Engine.runAgentWithProvider engineConfig prov agentConfig prompt
+
+      case result of
+        Left err -> pure <| Left err
+        Right response -> case Engine.resultError response of
+          Just err -> pure <| Left err
+          Nothing -> pure <| Right (Engine.resultFinalMessage response)
+
+-- | System prompt for the coder agent
+coderSystemPrompt :: Text
+coderSystemPrompt =
+  Text.unlines
+    [ "You are a coding agent. Your job is to implement code changes for tasks.",
+      "",
+      "## Process",
+      "",
+      "1. Read AGENTS.md to understand project conventions",
+      "2. Load the Coder skill from Omni/Ide/Coder.md for detailed instructions",
+      "3. Understand the task requirements",
+      "4. Make the necessary code changes",
+      "5. Run `bild` to verify compilation",
+      "6. Fix any errors until the code compiles",
+      "",
+      "## Guidelines",
+      "",
+      "- Be thorough but focused on the task",
+      "- Always verify your changes compile",
+      "- Do NOT commit - a reviewer will handle that"
+    ]
+
+-- | Resolve provider for coder (prefers claude-code if available)
+resolveCoderProvider :: IO (Either Text Provider.Provider)
+resolveCoderProvider = do
+  -- Check for explicit default provider
+  defaultProvider <- Environment.lookupEnv "AGENT_DEFAULT_PROVIDER"
+  case defaultProvider of
+    Just "claude-code" -> resolveClaudeCode
+    Just "anthropic" -> resolveAnthropic
+    Just "openrouter" -> resolveOpenRouter
+    _ -> do
+      -- Try claude-code first (cheapest with Claude Max)
+      ccResult <- resolveClaudeCode
+      case ccResult of
+        Right prov -> pure <| Right prov
+        Left _ -> do
+          -- Fall back to other providers
+          anthropicResult <- resolveAnthropic
+          case anthropicResult of
+            Right prov -> pure <| Right prov
+            Left _ -> resolveOpenRouter
+  where
+    resolveClaudeCode = do
+      tokenResult <- Auth.getValidToken
+      case tokenResult of
+        Right token -> pure <| Right <| Provider.anthropicOAuthProvider token "claude-sonnet-4-20250514"
+        Left err -> pure <| Left err
+
+    resolveAnthropic = do
+      mKey <- Environment.lookupEnv "ANTHROPIC_API_KEY"
+      case mKey of
+        Just key -> pure <| Right <| Provider.anthropicProvider (Text.pack key) "claude-sonnet-4-20250514"
+        Nothing -> pure <| Left "ANTHROPIC_API_KEY not set"
+
+    resolveOpenRouter = do
+      mKey <- Environment.lookupEnv "OPENROUTER_API_KEY"
+      case mKey of
+        Just key -> pure <| Right <| Provider.openRouterProvider (Text.pack key) "anthropic/claude-sonnet-4"
+        Nothing -> pure <| Left "No provider available (need AGENT_DEFAULT_PROVIDER, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY)"
+
+-- | Verify compilation after coder changes
+verifyCompilation :: CoderConfig -> Task.Task -> IO (Either Text ())
+verifyCompilation cfg task = do
+  case Task.taskNamespace task of
+    Nothing -> pure <| Right () -- No namespace, skip verification
+    Just ns -> do
+      let nsPath = Text.unpack ns
+      isFile <- Directory.doesFileExist nsPath
+      isDir <- Directory.doesDirectoryExist nsPath
+
+      if isDir
+        then do
+          coderOnActivity cfg <| "Skipping bild verification for " <> ns <> " (directory)"
+          pure <| Right ()
+        else
+          if not isFile
+            then pure <| Right () -- File doesn't exist, skip
+            else case Text.takeWhileEnd (/= '.') ns of
+              fileExt
+                | fileExt `elem` ["hs", "py", "nix", "lisp"] -> do
+                    coderOnActivity cfg "Verifying compilation..."
+                    (exitCode, _, stderrOutput) <-
+                      Process.readProcessWithExitCode "bild" [nsPath] ""
+                    case exitCode of
+                      Exit.ExitSuccess -> do
+                        coderOnActivity cfg "Compilation verified"
+                        pure <| Right ()
+                      Exit.ExitFailure _ ->
+                        pure <| Left <| "Compilation failed:\n" <> Text.pack stderrOutput
+                | otherwise -> do
+                    coderOnActivity cfg <| "Skipping bild verification for " <> ns <> " (not a buildable file type)"
+                    pure <| Right ()