← Back to task

Commit 5fbcd92f

commit 5fbcd92ff85bc9cc0b752888f6d3498aafea0b2a
Author: Ben Sima <ben@bensima.com>
Date:   Sun Nov 30 00:36:51 2025

    Remove amp dependency entirely
    
    The build and tests pass. Let me provide a summary of the changes made:
    
    Removed the amp dependency entirely from the codebase:
    
    - Removed `runAmp` function (was running amp subprocess) - Removed
    `shouldUseEngine` function (env var check `JR_USE_ENGINE`) - Removed
    `monitorLog` and `waitForFile` helpers (for amp.log parsing) - Removed
    unused imports: `System.IO`, `Data.Text.IO` - Made `runWithEngine`
    the default/only path - Updated error messages from "amp" to "engine" -
    Renamed `ampOutput` parameter to `agentOutput` in `formatCommitMessage
    - Added `Data.IORef` import for `newIORef`, `modifyIORef'`, `readIORef`
    
    - Removed amp.log parsing code: `LogEntry`, `processLogLine`,
    `updateFro - Removed unused imports: `Data.Aeson`,
    `Data.ByteString.Lazy`, `Data.Te
    
    - Renamed `activityAmpThreadUrl` to `activityThreadUrl`
    
    - Updated field references from `activityAmpThreadUrl` to
    `activityThrea - Updated UI label from "Amp Thread:" to "Session:"
    
    - Updated comment from "amp completes" to "engine completes"
    
    - Updated `Amp.execute` to `Engine.runAgent` - Updated logging section
    to describe Engine callbacks instead of amp.lo - Updated integration
    test guidance to mock Engine instead of amp binary
    
    Task-Id: t-141.6

diff --git a/Omni/Agent/DESIGN.md b/Omni/Agent/DESIGN.md
index 0ee1004c..ae1f6b35 100644
--- a/Omni/Agent/DESIGN.md
+++ b/Omni/Agent/DESIGN.md
@@ -58,7 +58,7 @@ The Haskell implementation should replicate the logic of `start-worker.sh` but w
         - `Task.claim task`
         - `baseBranch <- Git.determineBaseBranch task` (Check dependencies)
         - `Git.checkoutTaskBranch task baseBranch` (Force checkout to clean untracked files)
-        - `Amp.execute prompt`
+        - `Engine.runAgent prompt` (Native LLM agent via OpenRouter)
         - `Git.commit`
         - `Git.checkoutBase`
         - `Task.submitReview task`
@@ -70,8 +70,8 @@ The Haskell implementation should replicate the logic of `start-worker.sh` but w
 - `agent status` checks if PID is alive.
 
 ### 4.3 Logging
-- Continue writing raw Amp logs to `_/llm/amp.log` in the worker directory.
-- `agent log` reads this file and applies the filtering logic (currently in `monitor.sh` jq script) using Haskell (Aeson).
+- The Engine module uses callbacks to report activity and costs in real-time.
+- `agent log` displays the status bar with worker progress information.
 - **UI Design**:
     - **Two-line Status**: The CLI should maintain two reserved lines at the bottom (or top) of the output for each worker:
         - **Line 1 (Meta)**: `[Worker: omni-worker-1] Task: t-123 | Files: 3 | Credits: $0.45 | Time: 05:23`
@@ -109,7 +109,7 @@ The Haskell implementation should replicate the logic of `start-worker.sh` but w
 ### 6.2 Integration Tests
 - Create a temporary test repo.
 - Spawn a worker.
-- Mock `amp` binary (simple script that `echo "done"`).
+- Mock the Engine LLM calls or use a test API key.
 - Verify task moves from Open -> InProgress -> Review.
 
 ## 7. References
diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs
index 55bc1e25..46ea009c 100644
--- a/Omni/Agent/Log.hs
+++ b/Omni/Agent/Log.hs
@@ -6,12 +6,8 @@
 module Omni.Agent.Log where
 
 import Alpha
-import Data.Aeson ((.:), (.:?))
-import qualified Data.Aeson as Aeson
-import qualified Data.ByteString.Lazy as BL
 import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)
 import qualified Data.Text as Text
-import qualified Data.Text.Encoding as TE
 import qualified Data.Text.IO as TIO
 import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime)
 import Data.Time.Format (defaultTimeLocale, parseTimeOrError)
@@ -146,44 +142,6 @@ render = do
   ANSI.hCursorUp IO.stderr 4
   IO.hFlush IO.stderr
 
--- | Log Entry from JSON
-data LogEntry = LogEntry
-  { leMessage :: Text,
-    leThreadId :: Maybe Text,
-    leCredits :: Maybe Double,
-    leTotalCredits :: Maybe Double,
-    leTimestamp :: Maybe Text
-  }
-  deriving (Show, Eq)
-
-instance Aeson.FromJSON LogEntry where
-  parseJSON =
-    Aeson.withObject "LogEntry" <| \v ->
-      (LogEntry </ (v .: "message"))
-        <*> v
-        .:? "threadId"
-        <*> v
-        .:? "credits"
-        <*> v
-        .:? "totalCredits"
-        <*> v
-        .:? "timestamp"
-
--- | Parse a log line and update status
-processLogLine :: Text -> IO ()
-processLogLine line = do
-  let bs = BL.fromStrict <| TE.encodeUtf8 line
-  case Aeson.decode bs of
-    Just entry -> update (updateFromEntry entry)
-    Nothing -> pure () -- Ignore invalid JSON
-
-updateFromEntry :: LogEntry -> Status -> Status
-updateFromEntry LogEntry {..} s =
-  s
-    { statusThread = leThreadId <|> statusThread s,
-      statusCredits = maybe (statusCredits s) (/ 100.0) leTotalCredits -- Only update if totalCredits is present
-    }
-
 -- | Format elapsed time as MM:SS or HH:MM:SS
 formatElapsed :: NominalDiffTime -> Text
 formatElapsed elapsed =
diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs
index aa7c5ab0..ded4144e 100644
--- a/Omni/Agent/Worker.hs
+++ b/Omni/Agent/Worker.hs
@@ -7,10 +7,10 @@ import Alpha
 import qualified Data.Aeson as Aeson
 import qualified Data.Aeson.Key as AesonKey
 import qualified Data.ByteString.Lazy as BSL
+import Data.IORef (modifyIORef', newIORef, readIORef)
 import qualified Data.List as List
 import qualified Data.Text as Text
 import qualified Data.Text.Encoding as TE
-import qualified Data.Text.IO as TIO
 import qualified Data.Time
 import qualified Omni.Agent.Core as Core
 import qualified Omni.Agent.Engine as Engine
@@ -22,7 +22,6 @@ import qualified System.Directory as Directory
 import qualified System.Environment as Env
 import qualified System.Exit as Exit
 import System.FilePath ((</>))
-import qualified System.IO as IO
 import qualified System.Process as Process
 
 start :: Core.Worker -> Maybe Text -> IO ()
@@ -89,38 +88,18 @@ processTask worker task = do
   TaskCore.updateTaskStatus tid TaskCore.InProgress []
   say "[worker] Status -> InProgress"
 
-  -- Check if we should use native engine
-  useEngine <- shouldUseEngine
-
   -- Run agent with timing
   startTime <- Data.Time.getCurrentTime
   activityId <- TaskCore.logActivityWithMetrics tid TaskCore.Running Nothing Nothing (Just startTime) Nothing Nothing Nothing
 
-  (exitCode, output, maybeCost) <-
-    if useEngine
-      then do
-        say "[worker] Starting native engine..."
-        (code, out, cost) <- runWithEngine repo task
-        pure (code, out, Just cost)
-      else do
-        say "[worker] Starting amp..."
-        (code, out) <- runAmp repo task
-        pure (code, out, Nothing)
+  say "[worker] Starting engine..."
+  (exitCode, output, costCents) <- runWithEngine repo task
 
   endTime <- Data.Time.getCurrentTime
   say ("[worker] Agent exited with: " <> tshow exitCode)
 
-  -- Capture metrics - from engine result or agent log
-  (threadUrl, costCents) <- case maybeCost of
-    Just engineCost -> pure (Nothing, Just engineCost)
-    Nothing -> do
-      status <- AgentLog.getStatus
-      let url = ("https://ampcode.com/threads/" <>) </ AgentLog.statusThread status
-      let cost = Just <| floor (AgentLog.statusCredits status * 100)
-      pure (url, cost)
-
   -- Update the activity record with metrics
-  TaskCore.updateActivityMetrics activityId threadUrl (Just endTime) costCents Nothing
+  TaskCore.updateActivityMetrics activityId Nothing (Just endTime) (Just costCents) Nothing
 
   case exitCode of
     Exit.ExitSuccess -> do
@@ -178,10 +157,10 @@ processTask worker task = do
           say ("[worker] ✓ Task " <> tid <> " -> Review")
           unless quiet <| AgentLog.update (\s -> s {AgentLog.statusTask = Nothing})
     Exit.ExitFailure code -> do
-      say ("[worker] Amp failed with code " <> tshow code)
+      say ("[worker] Engine failed with code " <> tshow code)
       TaskCore.logActivity tid TaskCore.Failed (Just (toMetadata [("exit_code", tshow code)]))
       -- Don't set back to Open here - leave in InProgress for debugging
-      say "[worker] Task left in InProgress (amp failure)"
+      say "[worker] Task left in InProgress (engine failure)"
 
 -- | Run lint --fix to format and fix lint issues
 runFormatters :: FilePath -> IO (Either Text ())
@@ -218,115 +197,7 @@ tryCommit repo msg = do
             Exit.ExitFailure _ -> pure <| CommitFailed (Text.pack commitErr)
         Exit.ExitFailure c -> pure <| CommitFailed ("git diff failed with code " <> tshow c)
 
-runAmp :: FilePath -> TaskCore.Task -> IO (Exit.ExitCode, Text)
-runAmp repo task = do
-  -- Check for retry context
-  maybeRetry <- TaskCore.getRetryContext (TaskCore.taskId task)
-
-  let ns = fromMaybe "." (TaskCore.taskNamespace task)
-  let basePrompt =
-        "You are a Worker Agent.\n"
-          <> "Your goal is to implement the following task:\n\n"
-          <> formatTask task
-          <> "\n\nCRITICAL INSTRUCTIONS:\n"
-          <> "1. Analyze the codebase to understand where to make changes.\n"
-          <> "2. Implement the changes by editing files.\n"
-          <> "3. BEFORE finishing, you MUST run: bild --test "
-          <> ns
-          <> "\n"
-          <> "4. Fix ALL errors from bild --test (including hlint suggestions).\n"
-          <> "5. Keep running bild --test until it passes with no errors.\n"
-          <> "6. Do NOT update task status or manage git.\n"
-          <> "7. Only exit after bild --test passes.\n\n"
-          <> "IMPORTANT: The git commit will fail if hlint finds issues.\n"
-          <> "You must fix hlint suggestions like:\n"
-          <> "- 'Use list comprehension' -> use [x | cond] instead of if/else\n"
-          <> "- 'Avoid lambda' -> use function composition\n"
-          <> "- 'Redundant bracket' -> remove unnecessary parens\n\n"
-          <> "Context:\n"
-          <> "- Working directory: "
-          <> Text.pack repo
-          <> "\n"
-          <> "- Namespace: "
-          <> ns
-          <> "\n"
-
-  -- Add retry context if present
-  let retryPrompt = case maybeRetry of
-        Nothing -> ""
-        Just ctx ->
-          "\n\n## RETRY CONTEXT (IMPORTANT)\n\n"
-            <> "This task was previously attempted but failed. Attempt: "
-            <> tshow (TaskCore.retryAttempt ctx)
-            <> "/3\n"
-            <> "Reason: "
-            <> TaskCore.retryReason ctx
-            <> "\n\n"
-            <> ( if null (TaskCore.retryConflictFiles ctx)
-                   then ""
-                   else
-                     "Conflicting files from previous attempt:\n"
-                       <> Text.unlines (map ("  - " <>) (TaskCore.retryConflictFiles ctx))
-                       <> "\n"
-               )
-            <> "Original commit: "
-            <> TaskCore.retryOriginalCommit ctx
-            <> "\n\n"
-            <> maybe "" (\notes -> "## HUMAN NOTES/GUIDANCE\n\n" <> notes <> "\n\n") (TaskCore.retryNotes ctx)
-            <> "INSTRUCTIONS FOR RETRY:\n"
-            <> "- The codebase has changed since your last attempt\n"
-            <> "- Re-implement this task on top of the CURRENT codebase\n"
-            <> "- If there were merge conflicts, the conflicting files may have been modified by others\n"
-            <> "- Review the current state of those files before making changes\n"
-
-  let prompt = basePrompt <> retryPrompt
-
-  let logFile = repo </> "_/llm/amp.log"
-
-  -- Read AGENTS.md
-  agentsMd <-
-    fmap (fromMaybe "") <| do
-      exists <- Directory.doesFileExist (repo </> "AGENTS.md")
-      if exists
-        then Just </ readFile (repo </> "AGENTS.md")
-        else pure Nothing
-
-  -- Get relevant facts from the knowledge base
-  relevantFacts <- getRelevantFacts task
-  let factsSection = formatFacts relevantFacts
-
-  let fullPrompt =
-        prompt
-          <> "\n\nREPOSITORY GUIDELINES (AGENTS.md):\n"
-          <> agentsMd
-          <> factsSection
-
-  -- Remove old log file
-  exists <- Directory.doesFileExist logFile
-  when exists (Directory.removeFile logFile)
-
-  Directory.createDirectoryIfMissing True (repo </> "_/llm")
-
-  -- Assume amp is in PATH
-  let args = ["--try-opus", "--log-level", "debug", "--log-file", "_/llm/amp.log", "--dangerously-allow-all", "-x", Text.unpack fullPrompt]
-
-  let cp = (Process.proc "amp" args) {Process.cwd = Just repo, Process.std_out = Process.CreatePipe}
-  (_, Just hOut, _, ph) <- Process.createProcess cp
-
-  tid <- forkIO <| monitorLog logFile ph
-
-  exitCode <- Process.waitForProcess ph
-  output <- TIO.hGetContents hOut
-  killThread tid
-  pure (exitCode, output)
-
--- | Check if we should use native engine instead of amp subprocess
-shouldUseEngine :: IO Bool
-shouldUseEngine = do
-  env <- Env.lookupEnv "JR_USE_ENGINE"
-  pure <| env == Just "1"
-
--- | Run task using native Engine instead of amp subprocess
+-- | Run task using native Engine
 -- Returns (ExitCode, output text, cost in cents)
 runWithEngine :: FilePath -> TaskCore.Task -> IO (Exit.ExitCode, Text, Int)
 runWithEngine repo task = do
@@ -338,7 +209,7 @@ runWithEngine repo task = do
       -- Check for retry context
       maybeRetry <- TaskCore.getRetryContext (TaskCore.taskId task)
 
-      -- Build the full prompt (same as runAmp)
+      -- Build the full prompt
       let ns = fromMaybe "." (TaskCore.taskNamespace task)
       let basePrompt = buildBasePrompt task ns repo
 
@@ -517,10 +388,10 @@ formatTask t =
     formatComment c = "  [" <> Text.pack (show (TaskCore.commentCreatedAt c)) <> "] " <> TaskCore.commentText c
 
 formatCommitMessage :: TaskCore.Task -> Text -> Text
-formatCommitMessage task ampOutput =
+formatCommitMessage task agentOutput =
   let tid = TaskCore.taskId task
       subject = cleanSubject (TaskCore.taskTitle task)
-      body = cleanBody ampOutput
+      body = cleanBody agentOutput
    in if Text.null body
         then subject <> "\n\nTask-Id: " <> tid
         else subject <> "\n\n" <> body <> "\n\nTask-Id: " <> tid
@@ -573,34 +444,3 @@ formatFact f =
            then ""
            else " [" <> Text.intercalate ", " (TaskCore.factRelatedFiles f) <> "]"
        )
-
-monitorLog :: FilePath -> Process.ProcessHandle -> IO ()
-monitorLog path ph = do
-  waitForFile path
-  IO.withFile path IO.ReadMode <| \h -> do
-    IO.hSetBuffering h IO.LineBuffering
-    go h
-  where
-    go h = do
-      eof <- IO.hIsEOF h
-      if eof
-        then do
-          mExit <- Process.getProcessExitCode ph
-          case mExit of
-            Nothing -> do
-              threadDelay 100000 -- 0.1s
-              go h
-            Just _ -> pure ()
-        else do
-          line <- TIO.hGetLine h
-          AgentLog.processLogLine line
-          go h
-
-waitForFile :: FilePath -> IO ()
-waitForFile path = do
-  exists <- Directory.doesFileExist path
-  if exists
-    then pure ()
-    else do
-      threadDelay 100000
-      waitForFile path
diff --git a/Omni/Jr.hs b/Omni/Jr.hs
index 49f94c8b..06909709 100755
--- a/Omni/Jr.hs
+++ b/Omni/Jr.hs
@@ -155,7 +155,7 @@ runLoop delaySec = do
             (task : _) -> do
               putText ""
               putText ("[loop] === Working on: " <> TaskCore.taskId task <> " ===")
-              -- Run worker (this blocks until amp completes)
+              -- Run worker (this blocks until the engine completes)
               absPath <- Directory.getCurrentDirectory
               let name = Text.pack (takeFileName absPath)
               let worker =
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index befda940..4e55c610 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -1725,11 +1725,11 @@ instance Lucid.ToHtml TaskDetailPage where
           renderAttempt totalAttempts (attemptNum, act) = do
             when (totalAttempts > 1)
               <| Lucid.div_ [Lucid.class_ "attempt-header"] (Lucid.toHtml ("Attempt " <> tshow attemptNum :: Text))
-            case TaskCore.activityAmpThreadUrl act of
+            case TaskCore.activityThreadUrl act of
               Nothing -> pure ()
               Just url ->
                 Lucid.div_ [Lucid.class_ "metric-row"] <| do
-                  Lucid.span_ [Lucid.class_ "metric-label"] "Amp Thread:"
+                  Lucid.span_ [Lucid.class_ "metric-label"] "Session:"
                   Lucid.a_ [Lucid.href_ url, Lucid.target_ "_blank", Lucid.class_ "amp-thread-btn"] "View in Amp ↗"
 
             case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of
@@ -2203,11 +2203,11 @@ instance Lucid.ToHtml TaskMetricsPartial where
       renderAttempt totalAttempts currentTime (attemptNum, act) = do
         when (totalAttempts > 1)
           <| Lucid.div_ [Lucid.class_ "attempt-header"] (Lucid.toHtml ("Attempt " <> tshow attemptNum :: Text))
-        case TaskCore.activityAmpThreadUrl act of
+        case TaskCore.activityThreadUrl act of
           Nothing -> pure ()
           Just url ->
             Lucid.div_ [Lucid.class_ "metric-row"] <| do
-              Lucid.span_ [Lucid.class_ "metric-label"] "Amp Thread:"
+              Lucid.span_ [Lucid.class_ "metric-label"] "Session:"
               Lucid.a_ [Lucid.href_ url, Lucid.target_ "_blank", Lucid.class_ "amp-thread-btn"] "View in Amp ↗"
 
         case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index 92936bb5..49c22479 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -120,7 +120,7 @@ data TaskActivity = TaskActivity
     activityStage :: ActivityStage,
     activityMessage :: Maybe Text,
     activityMetadata :: Maybe Text, -- JSON for extra data
-    activityAmpThreadUrl :: Maybe Text, -- Link to amp thread
+    activityThreadUrl :: Maybe Text, -- Link to agent session (unused with native Engine)
     activityStartedAt :: Maybe UTCTime, -- When work started
     activityCompletedAt :: Maybe UTCTime, -- When work completed
     activityCostCents :: Maybe Int, -- API cost in cents
@@ -340,7 +340,7 @@ instance SQL.ToRow TaskActivity where
       SQL.toField (activityStage a),
       SQL.toField (activityMessage a),
       SQL.toField (activityMetadata a),
-      SQL.toField (activityAmpThreadUrl a),
+      SQL.toField (activityThreadUrl a),
       SQL.toField (activityStartedAt a),
       SQL.toField (activityCompletedAt a),
       SQL.toField (activityCostCents a),