summaryrefslogtreecommitdiff
path: root/Omni/Agent
diff options
context:
space:
mode:
Diffstat (limited to 'Omni/Agent')
-rw-r--r--Omni/Agent/Git.hs139
-rw-r--r--Omni/Agent/Log.hs65
-rw-r--r--Omni/Agent/LogTest.hs72
3 files changed, 276 insertions, 0 deletions
diff --git a/Omni/Agent/Git.hs b/Omni/Agent/Git.hs
new file mode 100644
index 0000000..cbb63bd
--- /dev/null
+++ b/Omni/Agent/Git.hs
@@ -0,0 +1,139 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Git operations for the agent.
+--
+-- : out omni-agent-git
+-- : dep temporary
+module Omni.Agent.Git
+ ( checkout,
+ main,
+ test,
+ )
+where
+
+import Alpha
+import qualified Data.Text as Text
+import qualified Omni.Log as Log
+import Omni.Test ((@=?))
+import qualified Omni.Test as Test
+import qualified System.Directory as Directory
+import qualified System.Exit as Exit
+import qualified System.IO.Temp as Temp
+import qualified System.Process as Process
+
+main :: IO ()
+main = Test.run test
+
+test :: Test.Tree
+test =
+ Test.group
+ "Omni.Agent.Git"
+ [ Test.unit "checkout works" <| do
+ Temp.withSystemTempDirectory "omni-agent-git-test" <| \tmpDir -> do
+ let repo = tmpDir <> "/repo"
+ Directory.createDirectory repo
+ -- init repo
+ git repo ["init"]
+ git repo ["branch", "-m", "master"]
+ git repo ["config", "user.email", "you@example.com"]
+ git repo ["config", "user.name", "Your Name"]
+
+ -- commit A
+ writeFile (repo <> "/a.txt") "A"
+ git repo ["add", "a.txt"]
+ git repo ["commit", "-m", "A"]
+ shaA <- getSha repo "HEAD"
+
+ -- create branch dev
+ git repo ["checkout", "-b", "dev"]
+
+ -- commit B
+ writeFile (repo <> "/b.txt") "B"
+ git repo ["add", "b.txt"]
+ git repo ["commit", "-m", "B"]
+ shaB <- getSha repo "HEAD"
+
+ -- switch back to master
+ git repo ["checkout", "master"]
+
+ -- Test 1: checkout dev
+ checkout repo "dev"
+ current <- getSha repo "HEAD"
+ shaB @=? current
+
+ -- Test 2: checkout master
+ checkout repo "master"
+ current' <- getSha repo "HEAD"
+ shaA @=? current'
+
+ -- Test 3: dirty state
+ writeFile (repo <> "/a.txt") "DIRTY"
+ checkout repo "dev"
+ current'' <- getSha repo "HEAD"
+ shaB @=? current''
+ -- Verify dirty file is gone/overwritten (b.txt should exist, a.txt should be A from master? No, a.txt is in A and B)
+ -- Wait, in dev, a.txt is "A".
+ content <- readFile (repo <> "/a.txt")
+ "A" @=? content
+
+ -- Test 4: untracked file
+ writeFile (repo <> "/untracked.txt") "DELETE ME"
+ checkout repo "master"
+ exists <- Directory.doesFileExist (repo <> "/untracked.txt")
+ False @=? exists
+ ]
+
+getSha :: FilePath -> String -> IO String
+getSha dir ref = do
+ let cmd = (Process.proc "git" ["rev-parse", ref]) {Process.cwd = Just dir}
+ (code, out, _) <- Process.readCreateProcessWithExitCode cmd ""
+ case code of
+ Exit.ExitSuccess -> pure <| strip out
+ _ -> panic "getSha failed"
+
+-- | Checkout a specific ref (SHA, branch, tag) in the given repository path.
+-- This function ensures the repository is in the correct state by:
+-- 1. Fetching all updates
+-- 2. Checking out the ref (forcing overwrites of local changes)
+-- 3. Resetting hard to the ref (to ensure clean state)
+-- 4. Cleaning untracked files
+-- 5. Updating submodules
+checkout :: FilePath -> Text -> IO ()
+checkout repoPath ref = do
+ let r = Text.unpack ref
+
+ Log.info ["git", "checkout", ref, "in", Text.pack repoPath]
+
+ -- Fetch all refs to ensure we have the target
+ git repoPath ["fetch", "--all", "--tags"]
+
+ -- Checkout the ref, discarding local changes
+ git repoPath ["checkout", "--force", r]
+
+ -- Reset hard to ensure we are exactly at the target state
+ git repoPath ["reset", "--hard", r]
+
+ -- Remove untracked files and directories
+ git repoPath ["clean", "-fdx"]
+
+ -- Update submodules
+ git repoPath ["submodule", "update", "--init", "--recursive"]
+
+ Log.good ["git", "checkout", "complete"]
+ Log.br
+
+-- | Run a git command in the given directory.
+git :: FilePath -> [String] -> IO ()
+git dir args = do
+ let cmd = (Process.proc "git" args) {Process.cwd = Just dir}
+ (exitCode, out, err) <- Process.readCreateProcessWithExitCode cmd ""
+ case exitCode of
+ Exit.ExitSuccess -> pure ()
+ Exit.ExitFailure code -> do
+ Log.fail ["git command failed", Text.pack (show args), "code: " <> show code]
+ Log.info [Text.pack out]
+ Log.info [Text.pack err]
+ Log.br
+ panic <| "git command failed: git " <> show args
diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs
new file mode 100644
index 0000000..33b9722
--- /dev/null
+++ b/Omni/Agent/Log.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Omni.Agent.Log
+ ( LogEntry (..),
+ parseLine,
+ format,
+ )
+where
+
+import Alpha
+import Data.Aeson (FromJSON (..), (.:), (.:?))
+import qualified Data.Aeson as Aeson
+import qualified Data.ByteString.Lazy as BSL
+
+data LogEntry = LogEntry
+ { leMessage :: Text,
+ leLevel :: Maybe Text,
+ leToolName :: Maybe Text,
+ leBatches :: Maybe [[Text]],
+ leMethod :: Maybe Text,
+ lePath :: Maybe Text
+ }
+ deriving (Show, Eq, Generic)
+
+instance FromJSON LogEntry where
+ parseJSON = Aeson.withObject "LogEntry" <| \v ->
+ LogEntry
+ <$> v .: "message"
+ <*> v .:? "level"
+ <*> v .:? "toolName"
+ <*> v .:? "batches"
+ <*> v .:? "method"
+ <*> v .:? "path"
+
+parseLine :: Text -> Maybe LogEntry
+parseLine line = Aeson.decode <| BSL.fromStrict <| encodeUtf8 line
+
+format :: LogEntry -> Maybe Text
+format e =
+ case leMessage e of
+ "executing 1 tools in 1 batch(es)" ->
+ let tool = case leBatches e of
+ Just ((t : _) : _) -> t
+ _ -> "unknown"
+ in Just <| "🤖 THOUGHT: Planning tool execution (" <> tool <> ")"
+ "Tool Bash permitted - action: allow" ->
+ Just "🔧 TOOL: Bash command executed"
+ msg
+ | "Processing tool completion for ledger" == msg && isJust (leToolName e) ->
+ Just <| "✅ TOOL: " <> fromMaybe "" (leToolName e) <> " completed"
+ "ide-fs" ->
+ case leMethod e of
+ Just "readFile" -> Just <| "📂 READ: " <> fromMaybe "" (lePath e)
+ _ -> Nothing
+ "System prompt build complete (no changes)" ->
+ Just "🧠 THINKING..."
+ "System prompt build complete (first build)" ->
+ Just "🚀 STARTING new task context"
+ msg ->
+ case leLevel e of
+ Just "error" -> Just <| "❌ ERROR: " <> msg
+ _ -> Nothing
diff --git a/Omni/Agent/LogTest.hs b/Omni/Agent/LogTest.hs
new file mode 100644
index 0000000..0d085b1
--- /dev/null
+++ b/Omni/Agent/LogTest.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- : out agent-log-test
+module Omni.Agent.LogTest where
+
+import Alpha
+import Omni.Agent.Log
+import qualified Omni.Test as Test
+
+main :: IO ()
+main = Test.run tests
+
+tests :: Test.Tree
+tests =
+ Test.group
+ "Omni.Agent.Log"
+ [ Test.unit "Parse LogEntry" testParse,
+ Test.unit "Format LogEntry" testFormat
+ ]
+
+testParse :: IO ()
+testParse = do
+ let json = "{\"message\": \"executing 1 tools in 1 batch(es)\", \"batches\": [[\"grep\"]]}"
+ let expected =
+ LogEntry
+ { leMessage = "executing 1 tools in 1 batch(es)",
+ leLevel = Nothing,
+ leToolName = Nothing,
+ leBatches = Just [["grep"]],
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ parseLine json @?= Just expected
+
+testFormat :: IO ()
+testFormat = do
+ let entry =
+ LogEntry
+ { leMessage = "executing 1 tools in 1 batch(es)",
+ leLevel = Nothing,
+ leToolName = Nothing,
+ leBatches = Just [["grep"]],
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ format entry @?= Just "🤖 THOUGHT: Planning tool execution (grep)"
+
+ let entry2 =
+ LogEntry
+ { leMessage = "some random log",
+ leLevel = Nothing,
+ leToolName = Nothing,
+ leBatches = Nothing,
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ format entry2 @?= Nothing
+
+ let entry3 =
+ LogEntry
+ { leMessage = "some error",
+ leLevel = Just "error",
+ leToolName = Nothing,
+ leBatches = Nothing,
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ format entry3 @?= Just "❌ ERROR: some error"
+
+(@?=) :: (Eq a, Show a) => a -> a -> IO ()
+(@?=) = (Test.@?=)