← Back to task

Commit adc47481

commit adc47481f28b7d5887921493490dd3578df904a2
Author: Coder Agent <coder@agents.omni>
Date:   Wed Apr 15 22:46:26 2026

    refactor(agentd): remove dead Servant daemon scaffolding
    
    Delete unused Servant API types, handlers, and daemon HTTP integration tests.
    
    Keep CLI-focused runtime helpers and persistent control functions intact.
    
    Task-Id: t-806

diff --git a/Omni/Agentd/Daemon.hs b/Omni/Agentd/Daemon.hs
index 11f5aaa1..b2524c5e 100644
--- a/Omni/Agentd/Daemon.hs
+++ b/Omni/Agentd/Daemon.hs
@@ -1,26 +1,17 @@
-{-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeOperators #-}
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# OPTIONS_GHC -Wno-unused-top-binds #-}
 
--- | Agentd Daemon - HTTP API for agent lifecycle management.
+-- | Agentd runtime support for persistent sessions.
 --
--- Provides a long-running daemon process with:
--- - HTTP API for spawning/managing agents
--- - SQLite persistence for agent state
--- - Direct process management of agent stdin-mode processes
--- - Webhook notifications on agent completion
+-- Provides filesystem/systemd/db helpers used by the CLI control plane.
 --
 -- : out agentd-daemon
 -- : dep aeson
 -- : dep http-conduit
--- : dep servant
--- : dep servant-server
 -- : dep sqlite-simple
--- : dep warp
 -- : dep async
 -- : dep stm
 -- : dep time
@@ -68,9 +59,8 @@ module Omni.Agentd.Daemon
   )
 where
 
-import Alpha hiding (Handler, state)
+import Alpha hiding (state)
 import qualified Control.Concurrent.Async as Async
-import qualified Control.Concurrent.MVar as MVar
 import Control.Concurrent.STM (TVar)
 import qualified Control.Concurrent.STM as STM
 import qualified Control.Exception as Exception
@@ -80,7 +70,6 @@ import qualified Data.Aeson.Key as Key
 import qualified Data.Aeson.KeyMap as KeyMap
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as BL
-import qualified Data.IORef as IORef
 import qualified Data.List as List
 import qualified Data.Map.Strict as Map
 import qualified Data.Maybe as Maybe
@@ -94,15 +83,10 @@ import qualified Data.UUID as UUID
 import qualified Data.UUID.V4 as UUID
 import qualified Database.SQLite.Simple as SQL
 import qualified Network.HTTP.Simple as HTTP
-import qualified Network.HTTP.Types.Status as HttpStatus
-import qualified Network.Socket as Socket
-import qualified Network.Wai as Wai
-import qualified Network.Wai.Handler.Warp as Warp
 import qualified Omni.Agent.Models as Models
 import qualified Omni.Agent.Trace as Trace
 import qualified Omni.Agents.Summarize as Summarize
 import qualified Omni.Test as Test
-import Servant
 import qualified System.Directory as Dir
 import qualified System.Environment as Env
 import qualified System.Exit as Exit
@@ -431,29 +415,6 @@ instance ToJSON PersistentAgent where
         "db_info" .= paDbInfo pa
       ]
 
--- * Servant API
-
-type AgentAPI =
-  -- POST /agents - create + start a persistent agent
-  "agents" :> ReqBody '[JSON] SpawnRequest :> Post '[JSON] SpawnResponse
-    -- GET /agents - list all agents
-    :<|> "agents" :> Get '[JSON] [AgentInfo]
-    -- GET /agents/:id - get agent status
-    :<|> "agents" :> Capture "runId" Text :> Get '[JSON] AgentInfo
-    -- POST /agents/:id/send - send a message to agent
-    :<|> "agents" :> Capture "runId" Text :> "send" :> ReqBody '[JSON] SendRequest :> Post '[JSON] Aeson.Value
-    -- POST /agents/:id/stop - stop an agent
-    :<|> "agents" :> Capture "runId" Text :> "stop" :> Post '[JSON] Aeson.Value
-    -- DELETE /agents/:id - remove an agent config (archive DB history)
-    :<|> "agents" :> Capture "runId" Text :> Delete '[JSON] Aeson.Value
-    -- GET /agents/:id/messages - get conversation messages
-    :<|> "agents" :> Capture "runId" Text :> "messages" :> Get '[JSON] [Aeson.Value]
-    -- GET /health - health check
-    :<|> "health" :> Get '[JSON] Aeson.Value
-
-agentAPI :: Proxy AgentAPI
-agentAPI = Proxy
-
 -- * Server State
 
 -- | Running agent process info
@@ -2691,73 +2652,6 @@ getLiveStatus state runId = do
     Nothing -> pure Nothing
     Just ra -> Just </ STM.atomically (STM.readTVar (raLiveStatus ra))
 
--- * Servant Handlers
-
-server :: DaemonState -> Server AgentAPI
-server state =
-  spawnHandler state
-    :<|> listHandler state
-    :<|> statusHandler state
-    :<|> sendHandler state
-    :<|> stopHandler state
-    :<|> deleteHandler state
-    :<|> messagesHandler state
-    :<|> healthHandler
-
-spawnHandler :: DaemonState -> SpawnRequest -> Handler SpawnResponse
-spawnHandler state req = do
-  runId <- liftIO <| spawnAgentProcess state req
-  pure <| SpawnResponse runId "started"
-
-listHandler :: DaemonState -> Handler [AgentInfo]
-listHandler state = do
-  let dbPath = Just (dcDbPath (dsConfig state))
-  agents <- liftIO <| listPersistentAgents dbPath
-  pure (mapMaybe paDbInfo agents)
-
-statusHandler :: DaemonState -> Text -> Handler AgentInfo
-statusHandler state runId = do
-  let dbPath = Just (dcDbPath (dsConfig state))
-  mAgent <- liftIO <| getPersistentAgent dbPath runId
-  case mAgent of
-    Just pa -> case paDbInfo pa of
-      Just info -> pure info
-      Nothing -> throwError err404 {errBody = "Agent not found"}
-    Nothing -> throwError err404 {errBody = "Agent not found"}
-
-sendHandler :: DaemonState -> Text -> SendRequest -> Handler Aeson.Value
-sendHandler state runId req = do
-  sent <- liftIO <| sendToAgent state runId (sendMessage req)
-  if sent
-    then pure <| Aeson.object ["status" .= ("sent" :: Text)]
-    else throwError err404 {errBody = "Agent not found"}
-
-stopHandler :: DaemonState -> Text -> Handler Aeson.Value
-stopHandler state runId = do
-  stopped <- liftIO <| stopAgentProcess state runId
-  if stopped
-    then pure <| Aeson.object ["status" .= ("stopped" :: Text)]
-    else do
-      fallback <- liftIO <| stopPersistentAgent (Just (dcDbPath (dsConfig state))) runId
-      case fallback of
-        Right _ -> pure <| Aeson.object ["status" .= ("stopped" :: Text)]
-        Left _ -> throwError err404 {errBody = "Agent not found"}
-
-deleteHandler :: DaemonState -> Text -> Handler Aeson.Value
-deleteHandler state runId = do
-  let dbPath = Just (dcDbPath (dsConfig state))
-  result <- liftIO <| removePersistentAgent dbPath runId
-  case result of
-    Left err -> throwError err500 {errBody = BL.fromStrict (encodeUtf8 err)}
-    Right () -> pure <| Aeson.object ["status" .= ("removed" :: Text)]
-
-messagesHandler :: DaemonState -> Text -> Handler [Aeson.Value]
-messagesHandler state runId =
-  liftIO <| getPersistentMessages (Just (dcDbPath (dsConfig state))) runId
-
-healthHandler :: Handler Aeson.Value
-healthHandler = pure <| Aeson.object ["status" .= ("ok" :: Text)]
-
 -- | Generate an LLM title for an agent and store it in the DB.
 -- Called asynchronously; failures are silently ignored.
 generateAndStoreTitle :: SQL.Connection -> Text -> Text -> Text -> IO ()
@@ -2779,25 +2673,6 @@ runDaemon _ = do
   TextIO.hPutStrLn IO.stderr "agentd daemon mode has been removed. Use the agentd CLI control plane instead."
   Exit.exitWith (Exit.ExitFailure 1)
 
--- | Graceful shutdown: stop all running agents, close handles (t-564)
-gracefulShutdown :: DaemonState -> IO ()
-gracefulShutdown state = do
-  putText "Shutting down agentd daemon..."
-  running <- STM.atomically <| STM.readTVar (dsRunning state)
-  -- Stop all agents with a timeout
-  forM_ (Map.keys running) <| \runId -> do
-    putText <| "  Stopping agent: " <> runId
-    stopResult <- try @SomeException <| stopAgentProcess state runId
-    case stopResult of
-      Left err -> putText <| "  Warning: failed to stop " <> runId <> ": " <> tshow err
-      Right _ -> pure ()
-  -- Close database connection
-  closeResult <- try @SomeException <| SQL.close (dsDbConn state)
-  case closeResult of
-    Left err -> putText <| "  Warning: failed to close DB: " <> tshow err
-    Right _ -> pure ()
-  putText "Shutdown complete."
-
 -- * Tests
 
 main :: IO ()
@@ -3064,358 +2939,5 @@ test =
       Test.unit "notifyd unit template runs agentd notifyd" <| do
         let unit = renderNotifydSystemdUnit "/home/ben/.local/state/agentd-agents" "/home/ben/omni/live/_/bin/agentd"
         Test.assertBool "notifyd unit should launch notifyd subcommand" ("ExecStart=/home/ben/omni/live/_/bin/agentd notifyd" `Text.isInfixOf` unit)
-        Test.assertBool "notifyd unit should not include ~/.local/bin in PATH" (not ("%h/.local/bin" `Text.isInfixOf` unit)),
-      Test.group
-        "integration"
-        [ Test.unit "spawn completes and sends webhook" integrationSpawnWebhook,
-          Test.unit "send message updates summary" integrationSendMessage,
-          Test.unit "worktree created and cleaned" integrationWorktreeCleanup,
-          Test.unit "failure reports failed" integrationFailure
-        ]
+        Test.assertBool "notifyd unit should not include ~/.local/bin in PATH" (not ("%h/.local/bin" `Text.isInfixOf` unit))
     ]
-
--- * Integration test helpers
-
-data TestDaemonEnv = TestDaemonEnv
-  { tdeBaseUrl :: Text,
-    tdeWorkspace :: FilePath
-  }
-
-data WebhookEnv = WebhookEnv
-  { weUrl :: Text,
-    weEvents :: IORef.IORef [Aeson.Value]
-  }
-
-data AgentInfoPayload = AgentInfoPayload
-  { aipRunId :: Text,
-    aipStatus :: Text,
-    aipWorkspace :: Text,
-    aipSummary :: Maybe Text,
-    aipCostCents :: Maybe Int,
-    aipError :: Maybe Text
-  }
-  deriving (Show, Eq)
-
-instance Aeson.FromJSON AgentInfoPayload where
-  parseJSON =
-    Aeson.withObject "AgentInfoPayload" <| \v ->
-      AgentInfoPayload
-        </ (v Aeson..: "run_id")
-        <*> (v Aeson..: "status")
-        <*> (v Aeson..: "workspace")
-        <*> (v Aeson..:? "summary")
-        <*> (v Aeson..:? "cost_cents")
-        <*> (v Aeson..:? "error")
-
-newtype SpawnResponsePayload = SpawnResponsePayload
-  { srRunId :: Text
-  }
-  deriving (Show, Eq)
-
-instance Aeson.FromJSON SpawnResponsePayload where
-  parseJSON =
-    Aeson.withObject "SpawnResponsePayload" <| \v ->
-      SpawnResponsePayload </ (v Aeson..: "run_id")
-
-shouldRunIntegrationTests :: IO Bool
-shouldRunIntegrationTests = do
-  value <- Env.lookupEnv "RUN_AGENTD_INTEGRATION_TESTS"
-  pure (Maybe.isJust value)
-
-integrationSpawnWebhook :: Test.Assertion
-integrationSpawnWebhook = do
-  runTests <- shouldRunIntegrationTests
-  when runTests <| do
-    withWebhookServer <| \webhookEnv ->
-      withTestDaemon "spawn-webhook" <| \env -> do
-        runId <- spawnAgent env "hello" (Just (weUrl webhookEnv))
-        mInfo <- waitForAgentInfo env runId (Maybe.isJust <. aipSummary)
-        case mInfo of
-          Nothing -> Test.assertFailure "Timed out waiting for summary"
-          Just info -> do
-            Test.assertBool "summary should include prompt" <| maybe False (Text.isInfixOf "hello") (aipSummary info)
-            Test.assertBool "cost should be present" <| maybe False (> 0) (aipCostCents info)
-        startedOk <- waitForWebhookEventType (weEvents webhookEnv) "started"
-        idleOk <- waitForWebhookEventType (weEvents webhookEnv) "idle"
-        Test.assertBool "expected started webhook" startedOk
-        Test.assertBool "expected idle webhook" idleOk
-        _ <- stopAgent env runId
-        pure ()
-
-integrationSendMessage :: Test.Assertion
-integrationSendMessage = do
-  runTests <- shouldRunIntegrationTests
-  when runTests <| do
-    withTestDaemon "send-message" <| \env -> do
-      runId <- spawnAgent env "first" Nothing
-      _ <- waitForAgentInfo env runId (maybe False (Text.isInfixOf "first") <. aipSummary)
-      _ <- sendAgentMessage env runId "second"
-      mInfo <- waitForAgentInfo env runId (maybe False (Text.isInfixOf "second") <. aipSummary)
-      case mInfo of
-        Nothing -> Test.assertFailure "Timed out waiting for updated summary"
-        Just info ->
-          Test.assertBool "summary should include second prompt" <| maybe False (Text.isInfixOf "second") (aipSummary info)
-      _ <- stopAgent env runId
-      pure ()
-
-integrationWorktreeCleanup :: Test.Assertion
-integrationWorktreeCleanup = do
-  runTests <- shouldRunIntegrationTests
-  when runTests <| do
-    withTempDir "worktree" <| \root -> do
-      let repoPath = root </> "repo"
-      initGitRepo repoPath
-      withTestDaemonForWorkspace "worktree" repoPath <| \env -> do
-        runId <- spawnAgent env "worktree" Nothing
-        mInfo <- waitForAgentInfo env runId (const True)
-        worktreePath <- case mInfo of
-          Nothing -> Test.assertFailure "Timed out waiting for agent info" >> pure ""
-          Just info -> pure (Text.unpack (aipWorkspace info))
-        absRepo <- Dir.makeAbsolute repoPath
-        let expectedPrefix = absRepo </> "_/agentd/worktrees/"
-        Test.assertBool "worktree path should be under repo" (expectedPrefix `List.isPrefixOf` worktreePath)
-        exists <- Dir.doesDirectoryExist worktreePath
-        Test.assertBool "worktree should exist" exists
-        _ <- stopAgent env runId
-        removed <- waitForPathRemoval worktreePath
-        Test.assertBool "worktree should be removed" removed
-
-integrationFailure :: Test.Assertion
-integrationFailure = do
-  runTests <- shouldRunIntegrationTests
-  when runTests <| do
-    withTestDaemon "failure" <| \env -> do
-      runId <- spawnAgent env "FAIL" Nothing
-      mInfo <- waitForAgentInfo env runId (\info -> aipStatus info == "failed")
-      case mInfo of
-        Nothing -> Test.assertFailure "Timed out waiting for failed status"
-        Just info ->
-          Test.assertBool "error should be present" <| Maybe.isJust (aipError info)
-      pure ()
-
-withTestDaemon :: Text -> (TestDaemonEnv -> IO a) -> IO a
-withTestDaemon label action =
-  withTempDir label <| \root -> do
-    let workspace = root </> "workspace"
-    Dir.createDirectoryIfMissing True workspace
-    withTestDaemonForWorkspace label workspace action
-
-withTestDaemonForWorkspace :: Text -> FilePath -> (TestDaemonEnv -> IO a) -> IO a
-withTestDaemonForWorkspace label workspace action =
-  withTempDir label <| \root -> do
-    absWorkspace <- Dir.makeAbsolute workspace
-    mockAgentPath <- writeMockAgent root
-    port <- pickFreePort
-    let dbPath = root </> "agentd.db"
-        logRoot = root </> "logs"
-        config =
-          DaemonConfig
-            { dcPort = port,
-              dcDbPath = dbPath,
-              dcLogRoot = logRoot,
-              dcWorkspace = absWorkspace,
-              dcAgentCmd = mockAgentPath
-            }
-        baseUrl = "http://127.0.0.1:" <> tshow port
-    daemonAsync <- Async.async (runDaemon config)
-    waitForHealth baseUrl
-    result <- action (TestDaemonEnv baseUrl absWorkspace)
-    Async.cancel daemonAsync
-    pure result
-
-withWebhookServer :: (WebhookEnv -> IO a) -> IO a
-withWebhookServer action = do
-  port <- pickFreePort
-  eventsRef <- IORef.newIORef []
-  ready <- MVar.newEmptyMVar
-  let app req respondFn = do
-        body <- Wai.strictRequestBody req
-        case Aeson.decode body of
-          Just val -> IORef.modifyIORef' eventsRef (\vals -> vals ++ [val])
-          Nothing -> pure ()
-        respondFn <| Wai.responseLBS HttpStatus.status200 [] "ok"
-      settings =
-        Warp.setPort port
-          <| Warp.setBeforeMainLoop (MVar.putMVar ready ()) Warp.defaultSettings
-  serverAsync <- Async.async (Warp.runSettings settings app)
-  MVar.takeMVar ready
-  let env = WebhookEnv {weUrl = "http://127.0.0.1:" <> tshow port, weEvents = eventsRef}
-  result <- action env
-  Async.cancel serverAsync
-  pure result
-
-spawnAgent :: TestDaemonEnv -> Text -> Maybe Text -> IO Text
-spawnAgent env prompt mWebhook = do
-  let payload =
-        Aeson.object
-          <| [ "prompt" .= prompt,
-               "workspace" .= Text.pack (tdeWorkspace env)
-             ]
-          ++ maybe [] (\url -> ["webhook" .= url]) mWebhook
-  req <- HTTP.parseRequest (Text.unpack (tdeBaseUrl env <> "/agents"))
-  let req' =
-        HTTP.setRequestBodyJSON payload
-          <| HTTP.setRequestMethod "POST" req
-  resp <- HTTP.httpJSON req' :: IO (HTTP.Response SpawnResponsePayload)
-  pure <| srRunId (HTTP.getResponseBody resp)
-
-sendAgentMessage :: TestDaemonEnv -> Text -> Text -> IO ()
-sendAgentMessage env runId message = do
-  let payload = Aeson.object ["message" .= message]
-  req <- HTTP.parseRequest (Text.unpack (tdeBaseUrl env <> "/agents/" <> runId <> "/send"))
-  let req' =
-        HTTP.setRequestBodyJSON payload
-          <| HTTP.setRequestMethod "POST" req
-  _ <- HTTP.httpNoBody req'
-  pure ()
-
-stopAgent :: TestDaemonEnv -> Text -> IO ()
-stopAgent env runId = do
-  req <- HTTP.parseRequest (Text.unpack (tdeBaseUrl env <> "/agents/" <> runId <> "/stop"))
-  let req' = HTTP.setRequestMethod "POST" req
-  _ <- HTTP.httpNoBody req'
-  pure ()
-
-getAgentInfo :: TestDaemonEnv -> Text -> IO (Maybe AgentInfoPayload)
-getAgentInfo env runId = do
-  result <-
-    try @SomeException <| do
-      req <- HTTP.parseRequest (Text.unpack (tdeBaseUrl env <> "/agents/" <> runId))
-      resp <- HTTP.httpJSON req :: IO (HTTP.Response AgentInfoPayload)
-      pure <| HTTP.getResponseBody resp
-  case result of
-    Left _ -> pure Nothing
-    Right info -> pure (Just info)
-
-waitForAgentInfo :: TestDaemonEnv -> Text -> (AgentInfoPayload -> Bool) -> IO (Maybe AgentInfoPayload)
-waitForAgentInfo env runId predicate = loop (40 :: Int)
-  where
-    loop 0 = pure Nothing
-    loop n = do
-      mInfo <- getAgentInfo env runId
-      case mInfo of
-        Just info | predicate info -> pure (Just info)
-        _ -> do
-          threadDelay 250000
-          loop (n - 1)
-
-waitForWebhookEventType :: IORef.IORef [Aeson.Value] -> Text -> IO Bool
-waitForWebhookEventType eventsRef eventType = loop (40 :: Int)
-  where
-    loop 0 = pure False
-    loop n = do
-      events <- IORef.readIORef eventsRef
-      let eventTypes = Maybe.mapMaybe extractEventType events
-      if eventType `elem` eventTypes
-        then pure True
-        else do
-          threadDelay 250000
-          loop (n - 1)
-
-waitForHealth :: Text -> IO ()
-waitForHealth baseUrl = loop (30 :: Int)
-  where
-    loop 0 = Test.assertFailure "agentd did not start"
-    loop n = do
-      result <-
-        try @SomeException <| do
-          req <- HTTP.parseRequest (Text.unpack (baseUrl <> "/health"))
-          resp <- HTTP.httpLBS req
-          pure (HTTP.getResponseStatusCode resp)
-      case result of
-        Right code | code >= 200 && code < 300 -> pure ()
-        _ -> do
-          threadDelay 200000
-          loop (n - 1)
-
-waitForPathRemoval :: FilePath -> IO Bool
-waitForPathRemoval path = loop (20 :: Int)
-  where
-    loop 0 = do
-      exists <- Dir.doesDirectoryExist path
-      pure (not exists)
-    loop n = do
-      exists <- Dir.doesDirectoryExist path
-      if exists
-        then do
-          threadDelay 200000
-          loop (n - 1)
-        else pure True
-
-extractEventType :: Aeson.Value -> Maybe Text
-extractEventType (Aeson.Object obj) = do
-  Aeson.String event <- KeyMap.lookup "event" obj
-  pure event
-extractEventType _ = Nothing
-
-withTempDir :: Text -> (FilePath -> IO a) -> IO a
-withTempDir label action = do
-  uuid <- UUID.nextRandom
-  let dir = "_/tmp/agentd-tests" </> Text.unpack label <> "-" <> Text.unpack (Text.take 8 (UUID.toText uuid))
-  absDir <- Dir.makeAbsolute dir
-  Dir.createDirectoryIfMissing True absDir
-  Exception.finally (action absDir) (Dir.removePathForcibly absDir)
-
-pickFreePort :: IO Int
-pickFreePort = do
-  sock <- Socket.socket Socket.AF_INET Socket.Stream Socket.defaultProtocol
-  Socket.setSocketOption sock Socket.ReuseAddr 1
-  Socket.bind sock (Socket.SockAddrInet 0 (Socket.tupleToHostAddress (127, 0, 0, 1)))
-  Socket.listen sock 1
-  Socket.SockAddrInet port _ <- Socket.getSocketName sock
-  Socket.close sock
-  pure (fromIntegral port)
-
-writeMockAgent :: FilePath -> IO FilePath
-writeMockAgent root = do
-  let path = root </> "mock-agent.sh"
-  TextIO.writeFile path mockAgentScript
-  _ <- Process.readProcessWithExitCode "chmod" ["+x", path] ""
-  pure path
-
-mockAgentScript :: Text
-mockAgentScript =
-  Text.unlines
-    [ "#!/usr/bin/env bash",
-      "set -euo pipefail",
-      "",
-      "ts='2026-01-01T00:00:00Z'",
-      "emit() {",
-      "  printf '%s\\n' \"$1\"",
-      "}",
-      "",
-      "while IFS= read -r -d '' prompt; do",
-      "  if [ -z \"$prompt\" ]; then",
-      "    continue",
-      "  fi",
-      "",
-      "  emit \"{\\\"type\\\":\\\"custom\\\",\\\"custom_type\\\":\\\"agent_start\\\",\\\"data\\\":{},\\\"timestamp\\\":\\\"${ts}\\\"}\"",
-      "",
-      "  if printf '%s' \"$prompt\" | grep -q 'FAIL'; then",
-      "    emit \"{\\\"type\\\":\\\"custom\\\",\\\"custom_type\\\":\\\"agent_error\\\",\\\"data\\\":{\\\"message\\\":\\\"Mock failure\\\"},\\\"timestamp\\\":\\\"${ts}\\\"}\"",
-      "    continue",
-      "  fi",
-      "",
-      "  emit \"{\\\"type\\\":\\\"infer_start\\\",\\\"model\\\":\\\"mock-model\\\",\\\"prompt_preview\\\":\\\"${prompt}\\\",\\\"timestamp\\\":\\\"${ts}\\\",\\\"iteration\\\":0}\"",
-      "  emit \"{\\\"type\\\":\\\"infer_end\\\",\\\"response_preview\\\":\\\"Mock reply: ${prompt}\\\",\\\"tokens\\\":42,\\\"cost_cents\\\":1.0,\\\"duration_ms\\\":5,\\\"timestamp\\\":\\\"${ts}\\\",\\\"iteration\\\":0}\"",
-      "  emit \"{\\\"type\\\":\\\"custom\\\",\\\"custom_type\\\":\\\"agent_complete\\\",\\\"data\\\":{\\\"response\\\":\\\"Mock reply: ${prompt}\\\"},\\\"timestamp\\\":\\\"${ts}\\\"}\"",
-      "done"
-    ]
-
-initGitRepo :: FilePath -> IO ()
-initGitRepo repoPath = do
-  Dir.createDirectoryIfMissing True repoPath
-  runGit repoPath ["init"]
-  runGit repoPath ["config", "user.email", "agentd@example.com"]
-  runGit repoPath ["config", "user.name", "Agentd Test"]
-  TextIO.writeFile (repoPath </> "README.md") "test"
-  runGit repoPath ["add", "README.md"]
-  runGit repoPath ["commit", "-m", "init"]
-  pure ()
-
-runGit :: FilePath -> [String] -> IO ()
-runGit repoPath args = do
-  (exitCode, _out, err) <- Process.readProcessWithExitCode "git" (["-C", repoPath] ++ args) ""
-  case exitCode of
-    Exit.ExitSuccess -> pure ()
-    Exit.ExitFailure _ -> Test.assertFailure <| "git failed: " <> err