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