From b5c6cfd04e5c1034e8d78830b55616f5540775e4 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Wed, 26 Nov 2025 05:53:00 -0500 Subject: Add jr loop command for autonomous work+review cycle - jr review [] [--auto]: auto-accepts if tests pass - jr review (no args): picks first task in Review status - jr loop [--delay=N]: runs work->review loop continuously - Auto-review runs bild --test on task namespace Task-Id: t-1o2g8gugkr1 --- Omni/Jr.hs | 191 +++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 153 insertions(+), 38 deletions(-) diff --git a/Omni/Jr.hs b/Omni/Jr.hs index f15a2b2..a940e82 100644 --- a/Omni/Jr.hs +++ b/Omni/Jr.hs @@ -51,7 +51,8 @@ Usage: jr task [...] jr work [] jr web [--port=PORT] - jr review + jr review [] [--auto] + jr loop [--delay=SECONDS] jr merge-driver jr test jr (-h | --help) @@ -61,11 +62,14 @@ Commands: work Start a worker agent on a task web Start the web UI server review Review a completed task (show diff, accept/reject) + loop Run autonomous work+review loop merge-driver Internal git merge driver Options: - -h --help Show this help - --port=PORT Port for web server [default: 8080] + -h --help Show this help + --port=PORT Port for web server [default: 8080] + --auto Auto-review: accept if tests pass, reject if they fail + --delay=SECONDS Delay between loop iterations [default: 5] |] move :: Cli.Arguments -> IO () @@ -98,8 +102,21 @@ move args AgentWorker.start worker taskId | args `Cli.has` Cli.command "review" = do - tidStr <- getArgOrExit args (Cli.argument "task-id") - reviewTask (Text.pack tidStr) + let autoMode = args `Cli.has` Cli.longOption "auto" + case Cli.getArg args (Cli.argument "task-id") of + Just tidStr -> reviewTask (Text.pack tidStr) autoMode + Nothing -> do + -- Find tasks in Review status + tasks <- TaskCore.loadTasks + let reviewTasks = filter (\t -> TaskCore.taskStatus t == TaskCore.Review) tasks + case reviewTasks of + [] -> putText "No tasks in Review status." + (t : _) -> reviewTask (TaskCore.taskId t) autoMode + | args `Cli.has` Cli.command "loop" = do + let delay = case Cli.getArg args (Cli.longOption "delay") of + Just d -> fromMaybe 5 (readMaybe d) + Nothing -> 5 + runLoop delay | args `Cli.has` Cli.command "merge-driver" = mergeDriver args | otherwise = putText (str <| Docopt.usage help) @@ -131,15 +148,59 @@ getArgOrExit args opt = putText <| "Error: Missing required argument " <> Text.pack (show opt) Exit.exitFailure -reviewTask :: Text -> IO () -reviewTask tid = do +-- | Run the autonomous loop: work -> review -> repeat +runLoop :: Int -> IO () +runLoop delaySec = do + putText "Starting autonomous jr loop..." + putText ("Delay between iterations: " <> tshow delaySec <> "s") + go + where + go = do + -- Check for ready work + readyTasks <- TaskCore.getReadyTasks + case readyTasks of + [] -> do + putText "\nNo ready tasks. Checking for tasks to review..." + reviewPending + (task : _) -> do + putText ("\n=== Working on: " <> TaskCore.taskId task <> " ===") + -- Run worker + absPath <- Directory.getCurrentDirectory + let name = Text.pack (takeFileName absPath) + let worker = + AgentCore.Worker + { AgentCore.workerName = name, + AgentCore.workerPid = Nothing, + AgentCore.workerStatus = AgentCore.Idle, + AgentCore.workerPath = "." + } + AgentWorker.start worker (Just (TaskCore.taskId task)) + -- After work, check for review + reviewPending + + -- Delay and loop + putText ("\nSleeping " <> tshow delaySec <> "s...") + threadDelay (delaySec * 1000000) + go + + reviewPending = do + tasks <- TaskCore.loadTasks + let reviewTasks = filter (\t -> TaskCore.taskStatus t == TaskCore.Review) tasks + case reviewTasks of + [] -> putText "No tasks pending review." + (t : _) -> do + putText ("\n=== Auto-reviewing: " <> TaskCore.taskId t <> " ===") + reviewTask (TaskCore.taskId t) True + +reviewTask :: Text -> Bool -> IO () +reviewTask tid autoMode = do tasks <- TaskCore.loadTasks case TaskCore.findTask tid tasks of Nothing -> do putText ("Task " <> tid <> " not found.") Exit.exitFailure Just task -> do - TaskCore.showTaskDetailed task + unless autoMode <| TaskCore.showTaskDetailed task let grepArg = "--grep=" <> Text.unpack tid (code, shaOut, _) <- @@ -191,38 +252,92 @@ reviewTask tid = do TaskCore.updateTaskStatus tid TaskCore.Open [] putText ("Task " <> tid <> " returned to queue (attempt " <> tshow attempt <> "/3).") Nothing -> do - -- No conflict, proceed with normal review - putText "\n=== Diff for this task ===\n" - _ <- Process.rawSystem "git" ["show", commitSha] + if autoMode + then autoReview tid task commitSha + else interactiveReview tid commitSha + +-- | Auto-review: run tests on namespace, accept if pass, reject if fail +autoReview :: Text -> TaskCore.Task -> String -> IO () +autoReview tid task commitSha = do + putText "Running automated review..." + + -- Determine what to test based on namespace + let namespace = fromMaybe "." (TaskCore.taskNamespace task) + let testTarget = Text.unpack (Text.replace ".hs" "" namespace) + + putText ("Testing: " <> Text.pack testTarget) + + -- Run bild --test on the namespace + (testCode, testOut, testErr) <- + Process.readProcessWithExitCode + "bild" + ["--test", testTarget] + "" - putText "\n[a]ccept / [r]eject / [s]kip? " + case testCode of + Exit.ExitSuccess -> do + putText "✓ Tests passed. Accepting task." + TaskCore.clearRetryContext tid + TaskCore.updateTaskStatus tid TaskCore.Done [] + putText ("Task " <> tid <> " marked as Done.") + Exit.ExitFailure _ -> do + putText "✗ Tests failed. Rejecting task." + let reason = "Test failure:\n" <> Text.pack testOut <> Text.pack testErr + + maybeCtx <- TaskCore.getRetryContext tid + let attempt = maybe 1 (\ctx -> TaskCore.retryAttempt ctx + 1) maybeCtx + + if attempt > 3 + then do + putText "\nTask has failed 3 times. Marking as NeedsHuman." + TaskCore.updateTaskStatus tid TaskCore.Open [] + putText ("Task " <> tid <> " needs human intervention (3 failed attempts).") + else do + TaskCore.setRetryContext + TaskCore.RetryContext + { TaskCore.retryTaskId = tid, + TaskCore.retryOriginalCommit = Text.pack commitSha, + TaskCore.retryConflictFiles = [], + TaskCore.retryAttempt = attempt, + TaskCore.retryReason = reason + } + TaskCore.updateTaskStatus tid TaskCore.Open [] + putText ("Task " <> tid <> " reopened (attempt " <> tshow attempt <> "/3).") + +-- | Interactive review with user prompts +interactiveReview :: Text -> String -> IO () +interactiveReview tid commitSha = do + putText "\n=== Diff for this task ===\n" + _ <- Process.rawSystem "git" ["show", commitSha] + + putText "\n[a]ccept / [r]eject / [s]kip? " + IO.hFlush IO.stdout + choice <- getLine + + case Text.toLower choice of + c + | "a" `Text.isPrefixOf` c -> do + TaskCore.clearRetryContext tid + TaskCore.updateTaskStatus tid TaskCore.Done [] + putText ("Task " <> tid <> " marked as Done.") + | "r" `Text.isPrefixOf` c -> do + putText "Enter rejection reason: " IO.hFlush IO.stdout - choice <- getLine - - case Text.toLower choice of - c - | "a" `Text.isPrefixOf` c -> do - TaskCore.clearRetryContext tid - TaskCore.updateTaskStatus tid TaskCore.Done [] - putText ("Task " <> tid <> " marked as Done.") - | "r" `Text.isPrefixOf` c -> do - putText "Enter rejection reason: " - IO.hFlush IO.stdout - reason <- getLine - -- Save rejection as retry context - maybeCtx <- TaskCore.getRetryContext tid - let attempt = maybe 1 (\ctx -> TaskCore.retryAttempt ctx + 1) maybeCtx - TaskCore.setRetryContext - TaskCore.RetryContext - { TaskCore.retryTaskId = tid, - TaskCore.retryOriginalCommit = Text.pack commitSha, - TaskCore.retryConflictFiles = [], - TaskCore.retryAttempt = attempt, - TaskCore.retryReason = "rejected: " <> reason - } - TaskCore.updateTaskStatus tid TaskCore.Open [] - putText ("Task " <> tid <> " reopened (attempt " <> tshow attempt <> "/3).") - | otherwise -> putText "Skipped; no status change." + reason <- getLine + -- Save rejection as retry context + maybeCtx <- TaskCore.getRetryContext tid + let attempt = maybe 1 (\ctx -> TaskCore.retryAttempt ctx + 1) maybeCtx + TaskCore.setRetryContext + TaskCore.RetryContext + { TaskCore.retryTaskId = tid, + TaskCore.retryOriginalCommit = Text.pack commitSha, + TaskCore.retryConflictFiles = [], + TaskCore.retryAttempt = attempt, + TaskCore.retryReason = "rejected: " <> reason + } + TaskCore.updateTaskStatus tid TaskCore.Open [] + putText ("Task " <> tid <> " reopened (attempt " <> tshow attempt <> "/3).") + | otherwise -> putText "Skipped; no status change." -- | Check if a commit can be cleanly cherry-picked onto live -- Returns Nothing if clean, Just [conflicting files] if conflict -- cgit v1.2.3