summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-22 17:07:32 -0500
committerBen Sima <ben@bensima.com>2025-11-22 17:08:11 -0500
commit832a0a7d88d0553e7edf055addb2c3a6f9f492ab (patch)
tree805e0aa0e174f2a3c61cb44fb3fe71562d77af2b
parent3e232940a6769cc2a238dc7b41b7c7b215295963 (diff)
parentbb15513a94140c22aa3aea510314f60c94df4d97 (diff)
task: complete t-1o2bxd11zv9 (Merge)
https: //ampcode.com/threads/T-ca3b086b-5a85-422a-b13d-256784c04221 Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-ca3b086b-5a85-422a-b13d-256784c04221
-rw-r--r--.tasks/tasks.jsonl2
-rw-r--r--Omni/Agent/Log.hs213
-rw-r--r--Omni/Agent/Worker.hs99
3 files changed, 131 insertions, 183 deletions
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl
index f481275..cd4dd0a 100644
--- a/.tasks/tasks.jsonl
+++ b/.tasks/tasks.jsonl
@@ -204,7 +204,7 @@
{"taskCreatedAt":"2025-11-22T20:37:13.980489314Z","taskDependencies":[],"taskDescription":"Configure .gitattributes and .git/config (via Omni/Ide/hooks or setup) to use 'agent merge-driver' for .tasks/tasks.jsonl. This prevents data loss when merging branches with divergent task lists.","taskId":"t-1o2bxcq7999.1","taskNamespace":"Omni/Ide.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P0","taskStatus":"Done","taskTitle":"Configure git merge driver for tasks.jsonl","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:57:23.592078308Z"}
{"taskCreatedAt":"2025-11-22T20:37:18.719690905Z","taskDependencies":[],"taskDescription":"Update Task Core to include Approved status, update CLI to support it, update TaskStats, and fix any compilation errors. Reference plan: /home/ben/omni/_/llm/PLAN_Autonomous_Workflow.md","taskId":"t-1o2bxcq7999.2","taskNamespace":"Omni/Task.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P1","taskStatus":"Done","taskTitle":"Add Approved status to Omni/Task","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:59:08.985299564Z"}
{"taskCreatedAt":"2025-11-22T20:37:23.378739333Z","taskDependencies":[],"taskDescription":"Rewrite Omni/Ci.sh into a robust Haskell program (Omni/Ci.hs). Reference plan: /home/ben/omni/_/llm/PLAN_Autonomous_Workflow.md","taskId":"t-1o2bxcq7999.3","taskNamespace":"Omni/Ci.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P1","taskStatus":"Done","taskTitle":"Implement Omni/Ci.hs","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T22:01:09.779228442Z"}
-{"taskCreatedAt":"2025-11-22T20:37:27.396872011Z","taskDependencies":[],"taskDescription":"The Time, Thread, and Credits fields in the agent status bar are not being populated. Update Omni/Agent/Log.hs to parse these fields from the JSON log output.","taskId":"t-1o2bxd11zv9","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Review","taskTitle":"Fix missing Time, Thread, and Credits in Agent Log","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:31:55.395825865Z"}
+{"taskCreatedAt":"2025-11-22T20:37:27.396872011Z","taskDependencies":[],"taskDescription":"The Time, Thread, and Credits fields in the agent status bar are not being populated. Update Omni/Agent/Log.hs to parse these fields from the JSON log output.","taskId":"t-1o2bxd11zv9","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Done","taskTitle":"Fix missing Time, Thread, and Credits in Agent Log","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T22:02:50.438643714Z"}
{"taskCreatedAt":"2025-11-22T20:37:31.615764727Z","taskDependencies":[],"taskDescription":"The 'task ready' command currently lists Epics. Update 'getReadyTasks' in Omni/Task/Core.hs to exclude tasks where taskType == Epic.","taskId":"t-1o2bxd3kezj","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Review","taskTitle":"Fix task ready to exclude Epics","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:34:28.577198837Z"}
{"taskCreatedAt":"2025-11-22T21:45:10.578083608Z","taskDependencies":[],"taskDescription":"Update Omni/Agent/start-worker.sh to run 'git sync' in the worker directory before building 'task' and 'agent'. This ensures the worker has the latest tools and code from live.","taskId":"t-1o2bxcq7999.4","taskNamespace":"Omni/Agent.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P1","taskStatus":"Done","taskTitle":"Sync worker repo in start-worker.sh","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T22:01:47.245671772Z"}
{"taskCreatedAt":"2025-11-22T21:19:54.675769476Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwd249bi3","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test Approved Status","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:20:10.652509625Z"}
diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs
index dd66abc..07770d0 100644
--- a/Omni/Agent/Log.hs
+++ b/Omni/Agent/Log.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
@@ -7,34 +6,22 @@
module Omni.Agent.Log where
import Alpha
-import Data.Aeson (Value (..), decode)
-import qualified Data.Aeson.KeyMap as KM
+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.Encoding as TextEnc
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as TE
import qualified Data.Text.IO as TIO
-import qualified Data.Vector as V
import qualified System.Console.ANSI as ANSI
import qualified System.IO as IO
import System.IO.Unsafe (unsafePerformIO)
-import Text.Printf (printf)
-
--- | Parsed log entry
-data LogEntry = LogEntry
- { leMessage :: Maybe Text,
- leLevel :: Maybe Text,
- leToolName :: Maybe Text,
- leBatches :: Maybe [[Text]],
- leMethod :: Maybe Text,
- lePath :: Maybe Text
- }
- deriving (Show, Eq)
-- | Status of the agent for the UI
data Status = Status
{ statusWorker :: Text,
statusTask :: Maybe Text,
- statusThreadId :: Maybe Text,
+ statusThread :: Maybe Text,
statusFiles :: Int,
statusCredits :: Double,
statusTime :: Text, -- formatted time string
@@ -47,7 +34,7 @@ emptyStatus workerName =
Status
{ statusWorker = workerName,
statusTask = Nothing,
- statusThreadId = Nothing,
+ statusThread = Nothing,
statusFiles = 0,
statusCredits = 0.0,
statusTime = "00:00",
@@ -64,9 +51,10 @@ init :: Text -> IO ()
init workerName = do
IO.hSetBuffering IO.stderr IO.LineBuffering
writeIORef currentStatus (emptyStatus workerName)
- -- Reserve 5 lines at bottom
- replicateM_ 5 (IO.hPutStrLn IO.stderr "")
- ANSI.hCursorUp IO.stderr 5
+ -- Reserve 2 lines at bottom
+ IO.hPutStrLn IO.stderr ""
+ IO.hPutStrLn IO.stderr ""
+ ANSI.hCursorUp IO.stderr 2
-- | Update the status
update :: (Status -> Status) -> IO ()
@@ -78,96 +66,14 @@ update f = do
updateActivity :: Text -> IO ()
updateActivity msg = update (\s -> s {statusActivity = msg})
--- | Process a log line from the agent and update status if relevant
-processLogLine :: Text -> IO ()
-processLogLine line = do
- let entry = parseLine line
- for_ (entry +> formatLogEntry) updateActivity
-
--- | Parse a JSON log line into a LogEntry
-parseLine :: Text -> Maybe LogEntry
-parseLine line = do
- let lbs = BL.fromStrict (TextEnc.encodeUtf8 line)
- obj <- decode lbs
- case obj of
- Object o ->
- Just
- LogEntry
- { leMessage = getString "message" o,
- leLevel = getString "level" o,
- leToolName = getString "toolName" o,
- leBatches = getBatches o,
- leMethod = getString "method" o,
- lePath = getString "path" o
- }
- _ -> Nothing
- where
- getString k o =
- case KM.lookup k o of
- Just (String s) -> Just s
- _ -> Nothing
-
- getBatches o =
- case KM.lookup "batches" o of
- Just (Array b) ->
- Just
- <| mapMaybe
- ( \case
- Array b0 ->
- Just
- <| mapMaybe
- ( \case
- String s -> Just s
- _ -> Nothing
- )
- (V.toList b0)
- _ -> Nothing
- )
- (V.toList b)
- _ -> Nothing
-
--- | Format a log entry into a user-friendly status message (NO EMOJIS)
-formatLogEntry :: LogEntry -> Maybe Text
-formatLogEntry LogEntry {..} =
- case leMessage of
- Just "executing 1 tools in 1 batch(es)" -> do
- let tools = fromMaybe [] leBatches
- let firstTool = case tools of
- ((t : _) : _) -> t
- _ -> "unknown"
- Just ("THOUGHT: Planning tool execution (" <> firstTool <> ")")
- Just "Tool Bash permitted - action: allow" ->
- Just "TOOL: Bash command executed"
- Just "Processing tool completion for ledger"
- | isJust leToolName ->
- Just ("TOOL: " <> fromMaybe "unknown" leToolName <> " completed")
- Just "ide-fs" | leMethod == Just "readFile" ->
- case lePath of
- Just p -> Just ("READ: " <> p)
- _ -> Nothing
- Just "System prompt build complete (no changes)" ->
- Just "THINKING..."
- Just "System prompt build complete (first build)" ->
- Just "STARTING new task context"
- Just msg
- | leLevel == Just "error" ->
- Just ("ERROR: " <> msg)
- _ -> Nothing
-
-- | Log a scrolling message (appears above status bars)
log :: Text -> IO ()
log msg = do
- -- Clear status bars (5 lines)
- ANSI.hClearLine IO.stderr
- ANSI.hCursorDown IO.stderr 1
- ANSI.hClearLine IO.stderr
- ANSI.hCursorDown IO.stderr 1
- ANSI.hClearLine IO.stderr
- ANSI.hCursorDown IO.stderr 1
+ -- Clear status bars
ANSI.hClearLine IO.stderr
ANSI.hCursorDown IO.stderr 1
ANSI.hClearLine IO.stderr
- ANSI.hCursorUp IO.stderr 4
+ ANSI.hCursorUp IO.stderr 1
-- Print message (scrolls screen)
TIO.hPutStrLn IO.stderr msg
@@ -176,43 +82,90 @@ log msg = do
-- (Since we scrolled, we are now on the line above where the first status line should be)
render
--- | Render the 5 status lines (Vertical Layout)
+-- | Render the two status lines
render :: IO ()
render = do
Status {..} <- readIORef currentStatus
+ -- Line 1: Meta
+ -- [Worker: name] Task: t-123 | Thread: T-abc | Files: 3 | Credits: $0.45 | Time: 05:23
let taskStr = maybe "None" identity statusTask
- threadStr = maybe "None" identity statusThreadId
+ threadStr = maybe "None" identity statusThread
+ meta =
+ "[Worker: "
+ <> statusWorker
+ <> "] Task: "
+ <> taskStr
+ <> " | Thread: "
+ <> threadStr
+ <> " | Files: "
+ <> tshow statusFiles
+ <> " | Credits: $"
+ <> tshow statusCredits
+ <> " | Time: "
+ <> statusTime
- -- Line 1: Worker + Time
ANSI.hSetCursorColumn IO.stderr 0
ANSI.hClearLine IO.stderr
- TIO.hPutStr IO.stderr <| "Worker: " <> statusWorker <> " | Time: " <> statusTime
+ TIO.hPutStr IO.stderr meta
- -- Line 2: Task
+ -- Line 2: Activity
+ -- [14:05:22] > Thinking...
ANSI.hCursorDown IO.stderr 1
ANSI.hSetCursorColumn IO.stderr 0
ANSI.hClearLine IO.stderr
- TIO.hPutStr IO.stderr <| "Task: " <> taskStr
+ TIO.hPutStr IO.stderr ("> " <> statusActivity)
- -- Line 3: Thread
- ANSI.hCursorDown IO.stderr 1
- ANSI.hSetCursorColumn IO.stderr 0
- ANSI.hClearLine IO.stderr
- TIO.hPutStr IO.stderr <| "Thread: " <> threadStr
+ -- Return cursor to line 1
+ ANSI.hCursorUp IO.stderr 1
+ IO.hFlush IO.stderr
- -- Line 4: Credits
- ANSI.hCursorDown IO.stderr 1
- ANSI.hSetCursorColumn IO.stderr 0
- ANSI.hClearLine IO.stderr
- TIO.hPutStr IO.stderr <| "Credits: $" <> str (printf "%.2f" statusCredits :: String)
+-- | Log Entry from JSON
+data LogEntry = LogEntry
+ { leMessage :: Text,
+ leThreadId :: Maybe Text,
+ leCredits :: Maybe Double,
+ leTotalCredits :: Maybe Double,
+ leTimestamp :: Maybe Text
+ }
+ deriving (Show, Eq)
- -- Line 5: Activity
- ANSI.hCursorDown IO.stderr 1
- ANSI.hSetCursorColumn IO.stderr 0
- ANSI.hClearLine IO.stderr
- TIO.hPutStr IO.stderr ("> " <> statusActivity)
+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 = fromMaybe (statusCredits s) (leTotalCredits <|> leCredits),
+ statusTime = maybe (statusTime s) formatTime leTimestamp
+ }
- -- Return cursor to Line 1
- ANSI.hCursorUp IO.stderr 4
- IO.hFlush IO.stderr
+formatTime :: Text -> Text
+formatTime ts =
+ -- "2025-11-22T21:24:02.512Z" -> "21:24"
+ case Text.splitOn "T" ts of
+ [_, time] -> case Text.splitOn ":" time of
+ (h : m : _) -> h <> ":" <> m
+ _ -> ts
+ _ -> ts
diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs
index 1cc0b8d..9f9e3bb 100644
--- a/Omni/Agent/Worker.hs
+++ b/Omni/Agent/Worker.hs
@@ -89,7 +89,7 @@ processTask worker task = do
-- Run Amp
AgentLog.updateActivity "Running Amp agent..."
- (exitCode, output) <- runAmp repo task
+ exitCode <- runAmp repo task
case exitCode of
Exit.ExitSuccess -> do
@@ -99,10 +99,8 @@ processTask worker task = do
TaskCore.updateTaskStatus tid TaskCore.Review []
-- Commit changes
- -- We use the agent's output as the extended commit description
- let summary = Text.strip output
- let commitMsg = "feat: implement " <> tid <> "\n\n" <> summary
- Git.commit repo commitMsg
+ -- We should check if there are changes, but 'git add .' is safe.
+ Git.commit repo ("feat: implement " <> tid)
-- Submit for review
AgentLog.updateActivity "Submitting for review..."
@@ -125,7 +123,7 @@ processTask worker task = do
AgentLog.updateActivity "Agent failed, retrying..."
threadDelay (10 * 1000000) -- Sleep 10s
-runAmp :: FilePath -> TaskCore.Task -> IO (Exit.ExitCode, Text)
+runAmp :: FilePath -> TaskCore.Task -> IO Exit.ExitCode
runAmp repo task = do
let prompt =
"You are a Worker Agent.\n"
@@ -137,8 +135,7 @@ runAmp repo task = do
<> "3. Run tests to verify your work (e.g., 'bild --test Omni/Namespace').\n"
<> "4. Fix any errors found during testing.\n"
<> "5. Do NOT update the task status or manage git branches (the system handles that).\n"
- <> "6. Do NOT run 'git commit'. The system will commit your changes automatically.\n"
- <> "7. When finished and tested, exit.\n\n"
+ <> "6. When finished and tested, exit.\n\n"
<> "Context:\n"
<> "- You are working in '"
<> Text.pack repo
@@ -147,38 +144,25 @@ runAmp repo task = do
<> fromMaybe "root" (TaskCore.taskNamespace task)
<> "'.\n"
- Directory.createDirectoryIfMissing True (repo </> "_/llm")
- let logPath = repo </> "_/llm/amp.log"
-
- -- Ensure log file is empty/exists
- IO.writeFile logPath ""
-
- -- Read AGENTS.md
- agentsMd <-
- fmap (fromMaybe "") <| do
- exists <- Directory.doesFileExist (repo </> "AGENTS.md")
- if exists
- then Just </ readFile (repo </> "AGENTS.md")
- else pure Nothing
+ let logFile = repo </> "_/llm/amp.log"
- let fullPrompt =
- prompt
- <> "\n\nREPOSITORY GUIDELINES (AGENTS.md):\n"
- <> agentsMd
+ -- Remove old log file
+ exists <- Directory.doesFileExist logFile
+ when exists (Directory.removeFile logFile)
- -- Monitor log file
- tidLog <- forkIO (monitorLog logPath)
+ Directory.createDirectoryIfMissing True (repo </> "_/llm")
-- Assume amp is in PATH
- let args = ["--log-level", "debug", "--log-file", "_/llm/amp.log", "--dangerously-allow-all", "-x", Text.unpack fullPrompt]
+ let args = ["--log-level", "debug", "--log-file", "_/llm/amp.log", "--dangerously-allow-all", "-x", Text.unpack prompt]
let cp = (Process.proc "amp" args) {Process.cwd = Just repo}
- (exitCode, out, _err) <- Process.readCreateProcessWithExitCode cp ""
+ (_, _, _, ph) <- Process.createProcess cp
- -- Cleanup
- killThread tidLog
+ tid <- forkIO <| monitorLog logFile ph
- pure (exitCode, Text.pack out)
+ exitCode <- Process.waitForProcess ph
+ killThread tid
+ pure exitCode
formatTask :: TaskCore.Task -> Text
formatTask t =
@@ -210,6 +194,37 @@ formatTask t =
where
formatDep dep = " - " <> TaskCore.depId dep <> " [" <> Text.pack (show (TaskCore.depType dep)) <> "]"
+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
+
findBaseBranch :: FilePath -> TaskCore.Task -> IO Text
findBaseBranch repo task = do
let deps = TaskCore.taskDependencies task
@@ -230,23 +245,3 @@ findBaseBranch repo task = do
case candidates of
(candidate : _) -> pure ("task/" <> TaskCore.depId candidate)
[] -> pure "live"
-
-monitorLog :: FilePath -> IO ()
-monitorLog path = do
- -- Wait for file to exist
- waitForFile path
-
- IO.withFile path IO.ReadMode <| \h -> do
- IO.hSetBuffering h IO.LineBuffering
- forever <| do
- eof <- IO.hIsEOF h
- if eof
- then threadDelay 100000 -- 0.1s
- else do
- line <- TIO.hGetLine h
- AgentLog.processLogLine line
-
-waitForFile :: FilePath -> IO ()
-waitForFile p = do
- e <- Directory.doesFileExist p
- if e then pure () else threadDelay 100000 >> waitForFile p