From 57635a9fbe5e217acd5ad609d7d9b97ce5cef232 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Fri, 21 Nov 2025 22:59:58 -0500 Subject: feat(task): support updating deps in task update Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Task.hs | 21 +++++++++++++++++++-- Omni/Task/Core.hs | 6 +++--- 2 files changed, 22 insertions(+), 5 deletions(-) (limited to 'Omni') diff --git a/Omni/Task.hs b/Omni/Task.hs index 36b318b..f19ab73 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -44,7 +44,7 @@ Usage: task list [options] task ready [--json] task show [--json] - task update [--json] + task update [--deps=] [--dep-type=] [--json] task deps [--json] task tree [] [--json] task progress [--json] @@ -214,13 +214,30 @@ move args | args `Cli.has` Cli.command "update" = do tid <- getArgText args "id" statusStr <- getArgText args "status" + + -- Handle update dependencies + (deps, depType) <- do + -- Parse --deps and --dep-type + ids <- case Cli.getArg args (Cli.longOption "deps") of + Nothing -> pure [] + Just depStr -> pure <| T.splitOn "," (T.pack depStr) + dtype <- case Cli.getArg args (Cli.longOption "dep-type") of + Nothing -> pure Blocks + Just "blocks" -> pure Blocks + Just "discovered-from" -> pure DiscoveredFrom + Just "parent-child" -> pure ParentChild + Just "related" -> pure Related + Just other -> panic <| "Invalid dependency type: " <> T.pack other <> ". Use: blocks, discovered-from, parent-child, or related" + pure (map (\d -> Dependency {depId = d, depType = dtype}) ids, dtype) + let newStatus = case statusStr of "open" -> Open "in-progress" -> InProgress "review" -> Review "done" -> Done _ -> panic "Invalid status. Use: open, in-progress, review, or done" - updateTaskStatus tid newStatus + + updateTaskStatus tid newStatus deps if isJsonMode args then outputSuccess <| "Updated task " <> tid else do diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index bab1912..9e4d2b4 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -343,15 +343,15 @@ createTask title taskType parent namespace priority deps description = pure task -- Update task status -updateTaskStatus :: Text -> Status -> IO () -updateTaskStatus tid newStatus = +updateTaskStatus :: Text -> Status -> [Dependency] -> IO () +updateTaskStatus tid newStatus newDeps = withTaskWriteLock <| do tasks <- loadTasksInternal now <- getCurrentTime let updatedTasks = map updateIfMatch tasks updateIfMatch t = if matchesId (taskId t) tid - then t {taskStatus = newStatus, taskUpdatedAt = now} + then t {taskStatus = newStatus, taskUpdatedAt = now, taskDependencies = if null newDeps then taskDependencies t else newDeps} else t -- Rewrite the entire file (simple approach for MVP) tasksFile <- getTasksFilePath -- cgit v1.2.3 From 029b1018432973f9631562268d01af8919f5113c Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Fri, 21 Nov 2025 23:02:34 -0500 Subject: feat(task): support updating dependencies via CLI Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Task.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'Omni') diff --git a/Omni/Task.hs b/Omni/Task.hs index f19ab73..5008dd2 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -44,7 +44,7 @@ Usage: task list [options] task ready [--json] task show [--json] - task update [--deps=] [--dep-type=] [--json] + task update [options] task deps [--json] task tree [] [--json] task progress [--json] @@ -214,9 +214,9 @@ move args | args `Cli.has` Cli.command "update" = do tid <- getArgText args "id" statusStr <- getArgText args "status" - + -- Handle update dependencies - (deps, depType) <- do + deps <- do -- Parse --deps and --dep-type ids <- case Cli.getArg args (Cli.longOption "deps") of Nothing -> pure [] @@ -228,7 +228,7 @@ move args Just "parent-child" -> pure ParentChild Just "related" -> pure Related Just other -> panic <| "Invalid dependency type: " <> T.pack other <> ". Use: blocks, discovered-from, parent-child, or related" - pure (map (\d -> Dependency {depId = d, depType = dtype}) ids, dtype) + pure (map (\d -> Dependency {depId = d, depType = dtype}) ids) let newStatus = case statusStr of "open" -> Open @@ -236,7 +236,7 @@ move args "review" -> Review "done" -> Done _ -> panic "Invalid status. Use: open, in-progress, review, or done" - + updateTaskStatus tid newStatus deps if isJsonMode args then outputSuccess <| "Updated task " <> tid -- cgit v1.2.3 From b2708836ef61bd7156fe4f3418444a54c00495cf Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 04:37:46 -0500 Subject: fix(agent): update updateTaskStatus calls Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Agent/Worker.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 01099a0..1961ea7 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -58,7 +58,7 @@ processTask worker task = do AgentLog.updateActivity ("Claiming task " <> tid) -- Claim task - TaskCore.updateTaskStatus tid TaskCore.InProgress + TaskCore.updateTaskStatus tid TaskCore.InProgress [] -- Commit claim locally Git.commit repo ("task: claim " <> tid) @@ -94,7 +94,7 @@ processTask worker task = do AgentLog.log "Agent finished successfully" -- Update status to Review (bundled with feature commit) - TaskCore.updateTaskStatus tid TaskCore.Review + TaskCore.updateTaskStatus tid TaskCore.Review [] -- Commit changes -- We should check if there are changes, but 'git add .' is safe. @@ -111,7 +111,7 @@ processTask worker task = do Git.syncWithLive repo -- Update status to Review (for signaling) - TaskCore.updateTaskStatus tid TaskCore.Review + TaskCore.updateTaskStatus tid TaskCore.Review [] Git.commit repo ("task: review " <> tid) AgentLog.log ("[✓] Task " <> tid <> " completed") -- cgit v1.2.3 From d97fe0e79e2970f0f58c889020d68f202ed11cdd Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 04:43:04 -0500 Subject: refactor(agent): use 'git sync' instead of rebase Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Agent/Git.hs | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Git.hs b/Omni/Agent/Git.hs index a2009b2..a33be23 100644 --- a/Omni/Agent/Git.hs +++ b/Omni/Agent/Git.hs @@ -25,7 +25,6 @@ import Omni.Test ((@=?)) import qualified Omni.Test as Test import qualified System.Directory as Directory import qualified System.Exit as Exit -import System.FilePath (()) import qualified System.IO.Temp as Temp import qualified System.Process as Process @@ -149,30 +148,18 @@ syncWithLive repo = do Log.info ["git", "syncing with live"] -- git repo ["fetch", "origin", "live"] -- Optional - -- Try rebase, if fail, abort - -- First, proactively cleanup any stale rebase state - cleanupStaleRebase repo - - let cmd = (Process.proc "git" ["rebase", "live"]) {Process.cwd = Just repo} - (code, _, err) <- Process.readCreateProcessWithExitCode cmd "" + -- Try sync (branchless sync), if fail, panic + -- This replaces manual rebase and handles stack movement + let cmd = (Process.proc "git" ["sync"]) {Process.cwd = Just repo} + (code, out, err) <- Process.readCreateProcessWithExitCode cmd "" case code of Exit.ExitSuccess -> pure () Exit.ExitFailure _ -> do - Log.warn ["rebase failed, aborting", Text.pack err] - cleanupStaleRebase repo - panic "Sync with live failed (rebase conflict)" - -cleanupStaleRebase :: FilePath -> IO () -cleanupStaleRebase repo = do - -- Check if a rebase is in progress - rebaseMerge <- Directory.doesDirectoryExist (repo ".git/rebase-merge") - rebaseApply <- Directory.doesDirectoryExist (repo ".git/rebase-apply") - - when (rebaseMerge || rebaseApply) <| do - Log.warn ["git", "detected stale rebase", "aborting"] - let abort = (Process.proc "git" ["rebase", "--abort"]) {Process.cwd = Just repo} - _ <- Process.readCreateProcessWithExitCode abort "" - pure () + Log.warn ["git sync failed", Text.pack err] + Log.info [Text.pack out] + panic "Sync with live failed (git sync)" + + commit :: FilePath -> Text -> IO () commit repo msg = do -- cgit v1.2.3 From c6dc12122fffad11cd40851589e13a25ad0d9291 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 04:45:37 -0500 Subject: fix(agent): fix updateTaskStatus signature Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Agent/Worker.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 1961ea7..85fd81a 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -113,10 +113,9 @@ processTask worker task = do -- Update status to Review (for signaling) TaskCore.updateTaskStatus tid TaskCore.Review [] Git.commit repo ("task: review " <> tid) - + AgentLog.log ("[✓] Task " <> tid <> " completed") AgentLog.update (\s -> s {AgentLog.statusTask = Nothing}) - Exit.ExitFailure code -> do AgentLog.log ("Agent failed with code " <> tshow code) AgentLog.updateActivity "Agent failed, retrying..." -- cgit v1.2.3 From fd3727cb87e6414affc2f05b49516658e4de28c6 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 04:45:48 -0500 Subject: refactor(agent): use git sync instead of rebase Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Agent/Git.hs | 2 -- 1 file changed, 2 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Git.hs b/Omni/Agent/Git.hs index a33be23..b1978f2 100644 --- a/Omni/Agent/Git.hs +++ b/Omni/Agent/Git.hs @@ -159,8 +159,6 @@ syncWithLive repo = do Log.info [Text.pack out] panic "Sync with live failed (git sync)" - - commit :: FilePath -> Text -> IO () commit repo msg = do Log.info ["git", "commit", msg] -- cgit v1.2.3 From e733b42ceb547015cba5c99a612857727b31d3c8 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 04:53:03 -0500 Subject: feat(agent): responsive 4-line status bar with thread ID Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Agent/Log.hs | 63 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 29 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs index afaf1da..1f2746d 100644 --- a/Omni/Agent/Log.hs +++ b/Omni/Agent/Log.hs @@ -16,6 +16,7 @@ import System.IO.Unsafe (unsafePerformIO) data Status = Status { statusWorker :: Text, statusTask :: Maybe Text, + statusThreadId :: Maybe Text, statusFiles :: Int, statusCredits :: Double, statusTime :: Text, -- formatted time string @@ -28,6 +29,7 @@ emptyStatus workerName = Status { statusWorker = workerName, statusTask = Nothing, + statusThreadId = Nothing, statusFiles = 0, statusCredits = 0.0, statusTime = "00:00", @@ -44,10 +46,9 @@ init :: Text -> IO () init workerName = do IO.hSetBuffering IO.stderr IO.LineBuffering writeIORef currentStatus (emptyStatus workerName) - -- Reserve 2 lines at bottom - IO.hPutStrLn IO.stderr "" - IO.hPutStrLn IO.stderr "" - ANSI.hCursorUp IO.stderr 2 + -- Reserve 4 lines at bottom + replicateM_ 4 (IO.hPutStrLn IO.stderr "") + ANSI.hCursorUp IO.stderr 4 -- | Update the status update :: (Status -> Status) -> IO () @@ -62,50 +63,54 @@ updateActivity msg = update (\s -> s {statusActivity = msg}) -- | Log a scrolling message (appears above status bars) log :: Text -> IO () log msg = do - -- Clear status bars + -- Clear status bars (4 lines) ANSI.hClearLine IO.stderr ANSI.hCursorDown IO.stderr 1 ANSI.hClearLine IO.stderr - ANSI.hCursorUp IO.stderr 1 - + ANSI.hCursorDown IO.stderr 1 + ANSI.hClearLine IO.stderr + ANSI.hCursorDown IO.stderr 1 + ANSI.hClearLine IO.stderr + ANSI.hCursorUp IO.stderr 3 + -- Print message (scrolls screen) TIO.hPutStrLn IO.stderr msg - + -- Re-render status bars at bottom -- (Since we scrolled, we are now on the line above where the first status line should be) render --- | Render the two status lines +-- | Render the 4 status lines (Vertical Layout) render :: IO () render = do Status {..} <- readIORef currentStatus - - -- Line 1: Meta - -- [Worker: name] Task: t-123 | Files: 3 | Credits: $0.45 | Time: 05:23 + let taskStr = maybe "None" identity statusTask - meta = - "[Worker: " - <> statusWorker - <> "] Task: " - <> taskStr - <> " | Files: " - <> tshow statusFiles - <> " | Credits: $" - <> tshow statusCredits - <> " | Time: " - <> statusTime + threadStr = maybe "None" identity statusThreadId + + -- Line 1: Worker + Time + ANSI.hSetCursorColumn IO.stderr 0 + ANSI.hClearLine IO.stderr + TIO.hPutStr IO.stderr <| "Worker: " <> statusWorker <> " | Time: " <> statusTime + -- Line 2: Task + ANSI.hCursorDown IO.stderr 1 ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr meta + TIO.hPutStr IO.stderr <| "Task: " <> taskStr - -- Line 2: Activity - -- [14:05:22] > Thinking... + -- Line 3: Thread + Credits ANSI.hCursorDown IO.stderr 1 ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr ("> " <> statusActivity) + TIO.hPutStr IO.stderr <| "Thread: " <> threadStr <> " | Credits: $" <> tshow statusCredits - -- Return cursor to line 1 - ANSI.hCursorUp IO.stderr 1 + -- Line 4: Activity + ANSI.hCursorDown IO.stderr 1 + ANSI.hSetCursorColumn IO.stderr 0 + ANSI.hClearLine IO.stderr + TIO.hPutStr IO.stderr ("> " <> statusActivity) + + -- Return cursor to Line 1 + ANSI.hCursorUp IO.stderr 3 IO.hFlush IO.stderr -- cgit v1.2.3 From 8b9a184260b5dfc563b884071d10511602606ccf Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 04:55:40 -0500 Subject: style: fix linting Amp-Thread-ID: https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483 Co-authored-by: Amp --- Omni/Agent/Log.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs index 1f2746d..084052b 100644 --- a/Omni/Agent/Log.hs +++ b/Omni/Agent/Log.hs @@ -72,10 +72,10 @@ log msg = do ANSI.hCursorDown IO.stderr 1 ANSI.hClearLine IO.stderr ANSI.hCursorUp IO.stderr 3 - + -- Print message (scrolls screen) TIO.hPutStrLn IO.stderr msg - + -- Re-render status bars at bottom -- (Since we scrolled, we are now on the line above where the first status line should be) render @@ -84,10 +84,10 @@ log msg = do render :: IO () render = do Status {..} <- readIORef currentStatus - + let taskStr = maybe "None" identity statusTask threadStr = maybe "None" identity statusThreadId - + -- Line 1: Worker + Time ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr @@ -110,7 +110,7 @@ render = do ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr TIO.hPutStr IO.stderr ("> " <> statusActivity) - + -- Return cursor to Line 1 ANSI.hCursorUp IO.stderr 3 IO.hFlush IO.stderr -- cgit v1.2.3 From 9bfabda73f3a65b0353670733a269963d335cf7e Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 05:05:34 -0500 Subject: feat: stream amp logs to worker status bar --- Omni/Agent/Worker.hs | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) (limited to 'Omni') diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 85fd81a..86c9e8b 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -5,7 +5,12 @@ module Omni.Agent.Worker where import Alpha +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as BL +import qualified Data.Scientific as Scientific import qualified Data.Text as Text +import qualified Data.Time as Time import qualified Omni.Agent.Core as Core import qualified Omni.Agent.Git as Git import qualified Omni.Agent.Log as AgentLog @@ -13,6 +18,7 @@ import qualified Omni.Task.Core as TaskCore import qualified System.Directory as Directory import qualified System.Exit as Exit import System.FilePath (()) +import qualified System.IO as IO import qualified System.Process as Process start :: Core.Worker -> IO () @@ -143,13 +149,28 @@ runAmp repo task = do <> "'.\n" Directory.createDirectoryIfMissing True (repo "_/llm") + let logFile = repo "_/llm/amp.log" + + -- Clean up previous log + exists <- Directory.doesFileExist logFile + when exists (Directory.removeFile logFile) + + -- Start background monitors + tidTime <- forkIO timeTicker + tidLog <- forkIO (monitorLog logFile) -- Assume amp is in PATH 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} (_, _, _, ph) <- Process.createProcess cp - Process.waitForProcess ph + exitCode <- Process.waitForProcess ph + + -- Cleanup + killThread tidTime + killThread tidLog + + pure exitCode formatTask :: TaskCore.Task -> Text formatTask t = @@ -201,3 +222,60 @@ findBaseBranch repo task = do case candidates of (candidate : _) -> pure ("task/" <> TaskCore.depId candidate) [] -> pure "live" + +monitorLog :: FilePath -> IO () +monitorLog logPath = do + waitForFile logPath + IO.withFile logPath IO.ReadMode <| \h -> do + -- Tail the file + IO.hSeek h IO.SeekFromEnd 0 + forever <| do + eof <- IO.hIsEOF h + if eof + then threadDelay 100000 -- 0.1s + else do + line <- IO.hGetLine h + parseAndUpdate (Text.pack line) + +waitForFile :: FilePath -> IO () +waitForFile path = do + exists <- Directory.doesFileExist path + if exists + then pure () + else do + threadDelay 100000 + waitForFile path + +parseAndUpdate :: Text -> IO () +parseAndUpdate line = do + let maybeObj = Aeson.decode (BL.fromStrict (encodeUtf8 line)) :: Maybe Aeson.Object + case maybeObj of + Nothing -> pure () + Just obj -> do + -- Extract msg + case KM.lookup "msg" obj of + Just (Aeson.String m) -> unless (Text.null m) (AgentLog.updateActivity m) + _ -> pure () + + -- Extract threadId + case KM.lookup "threadId" obj of + Just (Aeson.String tid) -> AgentLog.update (\s -> s {AgentLog.statusThreadId = Just tid}) + _ -> pure () + + -- Extract cost + case KM.lookup "usage" obj of + Just (Aeson.Object usage) -> + case KM.lookup "cost" usage of + Just (Aeson.Number n) -> + let cost = Scientific.toRealFloat n + in AgentLog.update (\s -> s {AgentLog.statusCredits = AgentLog.statusCredits s + cost}) + _ -> pure () + _ -> pure () + +timeTicker :: IO () +timeTicker = + forever <| do + time <- Time.getCurrentTime + let timeStr = Time.formatTime Time.defaultTimeLocale "%H:%M" time + AgentLog.update (\s -> s {AgentLog.statusTime = Text.pack timeStr}) + threadDelay 1000000 -- 1s -- cgit v1.2.3 From e4aaed813097dc880556ff8abc01af279b85f325 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 05:10:23 -0500 Subject: feat: split thread and credits lines in worker status bar Amp-Thread-ID: https://ampcode.com/threads/T-5a2ca80d-5cba-409f-a262-6b5c652c257d Co-authored-by: Amp --- Omni/Agent/Log.hs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs index 084052b..2e26272 100644 --- a/Omni/Agent/Log.hs +++ b/Omni/Agent/Log.hs @@ -46,9 +46,9 @@ init :: Text -> IO () init workerName = do IO.hSetBuffering IO.stderr IO.LineBuffering writeIORef currentStatus (emptyStatus workerName) - -- Reserve 4 lines at bottom - replicateM_ 4 (IO.hPutStrLn IO.stderr "") - ANSI.hCursorUp IO.stderr 4 + -- Reserve 5 lines at bottom + replicateM_ 5 (IO.hPutStrLn IO.stderr "") + ANSI.hCursorUp IO.stderr 5 -- | Update the status update :: (Status -> Status) -> IO () @@ -63,7 +63,9 @@ updateActivity msg = update (\s -> s {statusActivity = msg}) -- | Log a scrolling message (appears above status bars) log :: Text -> IO () log msg = do - -- Clear status bars (4 lines) + -- 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 @@ -71,7 +73,7 @@ log msg = do ANSI.hClearLine IO.stderr ANSI.hCursorDown IO.stderr 1 ANSI.hClearLine IO.stderr - ANSI.hCursorUp IO.stderr 3 + ANSI.hCursorUp IO.stderr 4 -- Print message (scrolls screen) TIO.hPutStrLn IO.stderr msg @@ -80,7 +82,7 @@ log msg = do -- (Since we scrolled, we are now on the line above where the first status line should be) render --- | Render the 4 status lines (Vertical Layout) +-- | Render the 5 status lines (Vertical Layout) render :: IO () render = do Status {..} <- readIORef currentStatus @@ -99,18 +101,24 @@ render = do ANSI.hClearLine IO.stderr TIO.hPutStr IO.stderr <| "Task: " <> taskStr - -- Line 3: Thread + Credits + -- Line 3: Thread ANSI.hCursorDown IO.stderr 1 ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr <| "Thread: " <> threadStr <> " | Credits: $" <> tshow statusCredits + TIO.hPutStr IO.stderr <| "Thread: " <> threadStr - -- Line 4: Activity + -- Line 4: Credits + ANSI.hCursorDown IO.stderr 1 + ANSI.hSetCursorColumn IO.stderr 0 + ANSI.hClearLine IO.stderr + TIO.hPutStr IO.stderr <| "Credits: $" <> tshow statusCredits + + -- Line 5: Activity ANSI.hCursorDown IO.stderr 1 ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr TIO.hPutStr IO.stderr ("> " <> statusActivity) -- Return cursor to Line 1 - ANSI.hCursorUp IO.stderr 3 + ANSI.hCursorUp IO.stderr 4 IO.hFlush IO.stderr -- cgit v1.2.3 From cf32eb4ea6e2f8594fea7895fee38c9c2a838042 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 05:14:49 -0500 Subject: fix: parse correct json fields from amp logs Amp-Thread-ID: https://ampcode.com/threads/T-5a2ca80d-5cba-409f-a262-6b5c652c257d Co-authored-by: Amp --- Omni/Agent/Worker.hs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 86c9e8b..4ff9042 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -252,8 +252,8 @@ parseAndUpdate line = do case maybeObj of Nothing -> pure () Just obj -> do - -- Extract msg - case KM.lookup "msg" obj of + -- Extract message (was msg) + case KM.lookup "message" obj of Just (Aeson.String m) -> unless (Text.null m) (AgentLog.updateActivity m) _ -> pure () @@ -262,14 +262,12 @@ parseAndUpdate line = do Just (Aeson.String tid) -> AgentLog.update (\s -> s {AgentLog.statusThreadId = Just tid}) _ -> pure () - -- Extract cost - case KM.lookup "usage" obj of - Just (Aeson.Object usage) -> - case KM.lookup "cost" usage of - Just (Aeson.Number n) -> - let cost = Scientific.toRealFloat n - in AgentLog.update (\s -> s {AgentLog.statusCredits = AgentLog.statusCredits s + cost}) - _ -> pure () + -- Extract cost from usage-ledger:event + -- Pattern: {"addedCredits": 0.123, "message": "usage-ledger:event", ...} + case KM.lookup "addedCredits" obj of + Just (Aeson.Number n) -> + let cost = Scientific.toRealFloat n + in AgentLog.update (\s -> s {AgentLog.statusCredits = AgentLog.statusCredits s + cost}) _ -> pure () timeTicker :: IO () -- cgit v1.2.3 From e97d41858c67ca7b019493be2b436e32b8069118 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 05:25:57 -0500 Subject: fix: read amp log from start and use totalCredits for accuracy Amp-Thread-ID: https://ampcode.com/threads/T-5a2ca80d-5cba-409f-a262-6b5c652c257d Co-authored-by: Amp --- Omni/Agent/Worker.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 4ff9042..7afc849 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -227,8 +227,7 @@ monitorLog :: FilePath -> IO () monitorLog logPath = do waitForFile logPath IO.withFile logPath IO.ReadMode <| \h -> do - -- Tail the file - IO.hSeek h IO.SeekFromEnd 0 + -- Start from beginning of file (don't seek to end) forever <| do eof <- IO.hIsEOF h if eof @@ -263,11 +262,12 @@ parseAndUpdate line = do _ -> pure () -- Extract cost from usage-ledger:event - -- Pattern: {"addedCredits": 0.123, "message": "usage-ledger:event", ...} - case KM.lookup "addedCredits" obj of + -- Pattern: {"totalCredits": 1.54, "message": "usage-ledger:event", ...} + -- We use totalCredits to be robust against missed lines and restarts. + case KM.lookup "totalCredits" obj of Just (Aeson.Number n) -> - let cost = Scientific.toRealFloat n - in AgentLog.update (\s -> s {AgentLog.statusCredits = AgentLog.statusCredits s + cost}) + let total = Scientific.toRealFloat n + in AgentLog.update (\s -> s {AgentLog.statusCredits = total}) _ -> pure () timeTicker :: IO () -- cgit v1.2.3 From 99175426feca3fda9117f20884b2294cee1c1bab Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 05:42:48 -0500 Subject: fix: divide amp credits by 100 to display dollars Amp-Thread-ID: https://ampcode.com/threads/T-5a2ca80d-5cba-409f-a262-6b5c652c257d Co-authored-by: Amp --- Omni/Agent/Worker.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'Omni') diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 7afc849..3504765 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -262,11 +262,11 @@ parseAndUpdate line = do _ -> pure () -- Extract cost from usage-ledger:event - -- Pattern: {"totalCredits": 1.54, "message": "usage-ledger:event", ...} - -- We use totalCredits to be robust against missed lines and restarts. + -- Pattern: {"totalCredits": 154.0, "message": "usage-ledger:event", ...} + -- The credits are in cents, so we divide by 100 to get dollars. case KM.lookup "totalCredits" obj of Just (Aeson.Number n) -> - let total = Scientific.toRealFloat n + let total = Scientific.toRealFloat n / 100.0 in AgentLog.update (\s -> s {AgentLog.statusCredits = total}) _ -> pure () -- cgit v1.2.3 From 7107e038ec661e5e121e226250f85771b0fd5ff4 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 06:36:53 -0500 Subject: feat: include AGENTS.md in worker prompt --- Omni/Agent/Worker.hs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'Omni') diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 3504765..a861173 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -151,6 +151,19 @@ runAmp repo task = do Directory.createDirectoryIfMissing True (repo "_/llm") let logFile = repo "_/llm/amp.log" + -- Read AGENTS.md + agentsMd <- + fmap (fromMaybe "") <| do + exists <- Directory.doesFileExist (repo "AGENTS.md") + if exists + then Just "AGENTS.md") + else pure Nothing + + let fullPrompt = + prompt + <> "\n\nREPOSITORY GUIDELINES (AGENTS.md):\n" + <> agentsMd + -- Clean up previous log exists <- Directory.doesFileExist logFile when exists (Directory.removeFile logFile) @@ -160,7 +173,7 @@ runAmp repo task = do tidLog <- forkIO (monitorLog logFile) -- Assume amp is in PATH - let args = ["--log-level", "debug", "--log-file", "_/llm/amp.log", "--dangerously-allow-all", "-x", Text.unpack prompt] + let args = ["--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} (_, _, _, ph) <- Process.createProcess cp -- cgit v1.2.3 From 242903ce18b3a85f45db1888f8944ab036d5df23 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 06:39:54 -0500 Subject: feat: implement t-rWcmRMaWX.4 --- Omni/Bild/audit.sh | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100755 Omni/Bild/audit.sh (limited to 'Omni') diff --git a/Omni/Bild/audit.sh b/Omni/Bild/audit.sh new file mode 100755 index 0000000..1a0acd5 --- /dev/null +++ b/Omni/Bild/audit.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +""" +Audit codebase builds. + +Iterates through every namespace in the project and runs 'bild'. +For every build failure encountered, it automatically creates a new task. +""" + +# : out bild-audit + +import os +import sys +import subprocess +import argparse +import shutil +import re + +# Extensions supported by bild (from Omni/Bild.hs and Omni/Namespace.hs) +EXTENSIONS = {'.c', '.hs', '.lisp', '.nix', '.py', '.scm', '.rs', '.toml'} + +def strip_ansi(text): + """Strip ANSI escape codes from text.""" + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + return ansi_escape.sub('', text) + +def is_ignored(path): + """Check if a file is ignored by git.""" + res = subprocess.run( + ['git', 'check-ignore', path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + return res.returncode == 0 + +def get_buildable_files(root_dir='.'): + """Find all files that bild can build.""" + targets = [] + for root, dirs, files in os.walk(root_dir): + # Skip hidden directories, build artifacts (_), and task db (.tasks) + dirs[:] = [d for d in dirs if not d.startswith('.') and d != '_'] + + for f in files: + ext = os.path.splitext(f)[1] + if ext in EXTENSIONS: + path = os.path.join(root, f) + # Clean up path + if path.startswith('./'): + path = path[2:] + + if not is_ignored(path): + targets.append(path) + return targets + +def run_bild(target): + """Run bild on the target.""" + # --time 0 disables timeout + # --loud enables output (which we capture) + cmd = ['bild', '--time', '0', '--loud', target] + result = subprocess.run(cmd, capture_output=True, text=True) + return result + +def create_task(target, result, parent_id=None): + """Create a task for a build failure.""" + # Construct a descriptive title + # Try to get the last meaningful line of error output + lines = (result.stdout + result.stderr).strip().split('\n') + last_line = lines[-1] if lines else "Unknown error" + last_line = strip_ansi(last_line).strip() + + if len(last_line) > 50: + last_line = last_line[:47] + "..." + + title = f"Build failed: {target} - {last_line}" + + cmd = [ + 'task', 'create', title, + '--priority', '2', + '--json' + ] + + if parent_id: + cmd.append(f'--discovered-from={parent_id}') + + # Try to infer namespace + parts = target.split('/') + if len(parts) > 1: + # e.g. Omni/Bild.hs -> Omni/Bild + # Omni/Bild/Audit.py -> Omni/Bild + ns = os.path.dirname(target) + if ns: + cmd.append(f'--namespace={ns}') + + print(f"Creating task for {target}...") + proc = subprocess.run(cmd, capture_output=True, text=True) + + if proc.returncode != 0: + print(f"Error creating task: {proc.stderr}", file=sys.stderr) + else: + # task create --json returns the created task json + print(f"Task created: {proc.stdout.strip()}") + +def main(): + parser = argparse.ArgumentParser(description='Audit codebase builds.') + parser.add_argument('--parent', help='Parent task ID to link discovered tasks to') + parser.add_argument('paths', nargs='*', default=['.'], help='Paths to search for targets') + args = parser.parse_args() + + # Check if bild is available + if not shutil.which('bild'): + print("Warning: 'bild' command not found. Ensure it is in PATH.", file=sys.stderr) + + print(f"Scanning for targets in {args.paths}...") + targets = [] + for path in args.paths: + if os.path.isfile(path): + targets.append(path) + else: + targets.extend(get_buildable_files(path)) + + # Remove duplicates + targets = sorted(list(set(targets))) + print(f"Found {len(targets)} targets.") + + failures = 0 + for i, target in enumerate(targets): + print(f"[{i+1}/{len(targets)}] Building {target} ... ", end='', flush=True) + res = run_bild(target) + + if res.returncode == 0: + print("OK") + else: + print("FAIL") + failures += 1 + create_task(target, res, args.parent) + + print(f"\nAudit complete. {failures} failures found.") + if failures > 0: + sys.exit(1) + else: + sys.exit(0) + +if __name__ == '__main__': + main() -- cgit v1.2.3 From fcc58e142eef98febdd385f65d4a28a869818110 Mon Sep 17 00:00:00 2001 From: Omni Worker Date: Sat, 22 Nov 2025 06:51:40 -0500 Subject: fix: rename to .py and fix lint errors Amp-Thread-ID: https://ampcode.com/threads/T-ec034efc-0ef1-43df-8abe-b0cda0f9115b Co-authored-by: Amp --- Omni/Bild/Audit.py | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Omni/Bild/audit.sh | 143 ------------------------------------------- 2 files changed, 176 insertions(+), 143 deletions(-) create mode 100755 Omni/Bild/Audit.py delete mode 100755 Omni/Bild/audit.sh (limited to 'Omni') diff --git a/Omni/Bild/Audit.py b/Omni/Bild/Audit.py new file mode 100755 index 0000000..4df6c0b --- /dev/null +++ b/Omni/Bild/Audit.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Audit codebase builds. + +Iterates through every namespace in the project and runs 'bild'. +For every build failure encountered, it automatically creates a new task. +""" + +# : out bild-audit + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + +# Extensions supported by bild (from Omni/Bild.hs and Omni/Namespace.hs) +EXTENSIONS = {".c", ".hs", ".lisp", ".nix", ".py", ".scm", ".rs", ".toml"} +MAX_TITLE_LENGTH = 50 + + +def strip_ansi(text: str) -> str: + """Strip ANSI escape codes from text.""" + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + return ansi_escape.sub("", text) + + +def is_ignored(path: Path) -> bool: + """Check if a file is ignored by git.""" + res = subprocess.run( + ["git", "check-ignore", str(path)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + return res.returncode == 0 + + +def get_buildable_files(root_dir: str = ".") -> list[str]: + """Find all files that bild can build.""" + targets: list[str] = [] + + root = Path(root_dir) + if not root.exists(): + return [] + + for path in root.rglob("*"): + # Skip directories + if path.is_dir(): + continue + + # Skip hidden files/dirs and '_' dirs + parts = path.parts + if any(p.startswith(".") or p == "_" for p in parts): + continue + + if path.suffix in EXTENSIONS: + # Clean up path: keep it relative to cwd if possible + try: + # We want the path as a string, relative to current directory + # if possible + p_str = ( + str(path.relative_to(Path.cwd())) + if path.is_absolute() + else str(path) + ) + except ValueError: + p_str = str(path) + + if not is_ignored(Path(p_str)): + targets.append(p_str) + return targets + + +def run_bild(target: str) -> subprocess.CompletedProcess[str]: + """Run bild on the target.""" + # --time 0 disables timeout + # --loud enables output (which we capture) + cmd = ["bild", "--time", "0", "--loud", target] + return subprocess.run(cmd, capture_output=True, text=True, check=False) + + +def create_task( + target: str, + result: subprocess.CompletedProcess[str], + parent_id: str | None = None, +) -> None: + """Create a task for a build failure.""" + # Construct a descriptive title + # Try to get the last meaningful line of error output + lines = (result.stdout + result.stderr).strip().split("\n") + last_line = lines[-1] if lines else "Unknown error" + last_line = strip_ansi(last_line).strip() + + if len(last_line) > MAX_TITLE_LENGTH: + last_line = last_line[: MAX_TITLE_LENGTH - 3] + "..." + + title = f"Build failed: {target} - {last_line}" + + cmd = ["task", "create", title, "--priority", "2", "--json"] + + if parent_id: + cmd.append(f"--discovered-from={parent_id}") + + # Try to infer namespace + # e.g. Omni/Bild.hs -> Omni/Bild + ns = Path(target).parent + if str(ns) != ".": + cmd.append(f"--namespace={ns}") + + print(f"Creating task for {target}...") # noqa: T201 + proc = subprocess.run(cmd, capture_output=True, text=True, check=False) + + if proc.returncode != 0: + print(f"Error creating task: {proc.stderr}", file=sys.stderr) # noqa: T201 + else: + # task create --json returns the created task json + print(f"Task created: {proc.stdout.strip()}") # noqa: T201 + + +def main() -> None: + """Run the build audit.""" + parser = argparse.ArgumentParser(description="Audit codebase builds.") + parser.add_argument( + "--parent", + help="Parent task ID to link discovered tasks to", + ) + parser.add_argument( + "paths", + nargs="*", + default=["."], + help="Paths to search for targets", + ) + args = parser.parse_args() + + # Check if bild is available + if not shutil.which("bild"): + print( # noqa: T201 + "Warning: 'bild' command not found. Ensure it is in PATH.", + file=sys.stderr, + ) + + print(f"Scanning for targets in {args.paths}...") # noqa: T201 + targets: list[str] = [] + for path_str in args.paths: + path = Path(path_str) + if path.is_file(): + targets.append(str(path)) + else: + targets.extend(get_buildable_files(path_str)) + + # Remove duplicates + targets = sorted(set(targets)) + print(f"Found {len(targets)} targets.") # noqa: T201 + + failures = 0 + for target in targets: + res = run_bild(target) + + if res.returncode == 0: + print("OK") # noqa: T201 + else: + print("FAIL") # noqa: T201 + failures += 1 + create_task(target, res, args.parent) + + print(f"\nAudit complete. {failures} failures found.") # noqa: T201 + if failures > 0: + sys.exit(1) + else: + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/Omni/Bild/audit.sh b/Omni/Bild/audit.sh deleted file mode 100755 index 1a0acd5..0000000 --- a/Omni/Bild/audit.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -""" -Audit codebase builds. - -Iterates through every namespace in the project and runs 'bild'. -For every build failure encountered, it automatically creates a new task. -""" - -# : out bild-audit - -import os -import sys -import subprocess -import argparse -import shutil -import re - -# Extensions supported by bild (from Omni/Bild.hs and Omni/Namespace.hs) -EXTENSIONS = {'.c', '.hs', '.lisp', '.nix', '.py', '.scm', '.rs', '.toml'} - -def strip_ansi(text): - """Strip ANSI escape codes from text.""" - ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') - return ansi_escape.sub('', text) - -def is_ignored(path): - """Check if a file is ignored by git.""" - res = subprocess.run( - ['git', 'check-ignore', path], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - return res.returncode == 0 - -def get_buildable_files(root_dir='.'): - """Find all files that bild can build.""" - targets = [] - for root, dirs, files in os.walk(root_dir): - # Skip hidden directories, build artifacts (_), and task db (.tasks) - dirs[:] = [d for d in dirs if not d.startswith('.') and d != '_'] - - for f in files: - ext = os.path.splitext(f)[1] - if ext in EXTENSIONS: - path = os.path.join(root, f) - # Clean up path - if path.startswith('./'): - path = path[2:] - - if not is_ignored(path): - targets.append(path) - return targets - -def run_bild(target): - """Run bild on the target.""" - # --time 0 disables timeout - # --loud enables output (which we capture) - cmd = ['bild', '--time', '0', '--loud', target] - result = subprocess.run(cmd, capture_output=True, text=True) - return result - -def create_task(target, result, parent_id=None): - """Create a task for a build failure.""" - # Construct a descriptive title - # Try to get the last meaningful line of error output - lines = (result.stdout + result.stderr).strip().split('\n') - last_line = lines[-1] if lines else "Unknown error" - last_line = strip_ansi(last_line).strip() - - if len(last_line) > 50: - last_line = last_line[:47] + "..." - - title = f"Build failed: {target} - {last_line}" - - cmd = [ - 'task', 'create', title, - '--priority', '2', - '--json' - ] - - if parent_id: - cmd.append(f'--discovered-from={parent_id}') - - # Try to infer namespace - parts = target.split('/') - if len(parts) > 1: - # e.g. Omni/Bild.hs -> Omni/Bild - # Omni/Bild/Audit.py -> Omni/Bild - ns = os.path.dirname(target) - if ns: - cmd.append(f'--namespace={ns}') - - print(f"Creating task for {target}...") - proc = subprocess.run(cmd, capture_output=True, text=True) - - if proc.returncode != 0: - print(f"Error creating task: {proc.stderr}", file=sys.stderr) - else: - # task create --json returns the created task json - print(f"Task created: {proc.stdout.strip()}") - -def main(): - parser = argparse.ArgumentParser(description='Audit codebase builds.') - parser.add_argument('--parent', help='Parent task ID to link discovered tasks to') - parser.add_argument('paths', nargs='*', default=['.'], help='Paths to search for targets') - args = parser.parse_args() - - # Check if bild is available - if not shutil.which('bild'): - print("Warning: 'bild' command not found. Ensure it is in PATH.", file=sys.stderr) - - print(f"Scanning for targets in {args.paths}...") - targets = [] - for path in args.paths: - if os.path.isfile(path): - targets.append(path) - else: - targets.extend(get_buildable_files(path)) - - # Remove duplicates - targets = sorted(list(set(targets))) - print(f"Found {len(targets)} targets.") - - failures = 0 - for i, target in enumerate(targets): - print(f"[{i+1}/{len(targets)}] Building {target} ... ", end='', flush=True) - res = run_bild(target) - - if res.returncode == 0: - print("OK") - else: - print("FAIL") - failures += 1 - create_task(target, res, args.parent) - - print(f"\nAudit complete. {failures} failures found.") - if failures > 0: - sys.exit(1) - else: - sys.exit(0) - -if __name__ == '__main__': - main() -- cgit v1.2.3