← Back to task

Commit 63051799

commit 6305179905d5428879aaf8c08fef1722c734c3d7
Author: Ben Sima <ben@bensima.com>
Date:   Wed Nov 26 13:55:42 2025

    Jr: Gerrit-style conflict handling - kick back to coder with context
    
    All tests pass and lint is clean. The implementation adds Gerrit-style
    c
    
    1. **`gatherConflictContext`** - Creates rich context including:
       - The commit info (SHA, subject, body) - Current HEAD state (what
       branch moved to) - Per-file conflict details showing both your
       changes and recent chan
    
    2. **`getFileConflictInfo`** - For each conflicting file, shows:
       - Your changes to that file (stat summary) - Recent changes by
       others (last 3 commits touching the file)
    
    3. The context is stored in `retryReason` and passed to the worker
    via t
    
    Task-Id: t-1o2g8gudqlx

diff --git a/Omni/Jr.hs b/Omni/Jr.hs
index 3ddb3b19..cd06a42e 100644
--- a/Omni/Jr.hs
+++ b/Omni/Jr.hs
@@ -208,7 +208,7 @@ runLoop delaySec = do
                 Nothing -> do
                   autoReview tid task commitSha
 
--- | Handle merge conflict during review
+-- | Handle merge conflict during review (Gerrit-style: provide rich context)
 handleConflict :: Text -> [Text] -> String -> IO ()
 handleConflict tid conflictFiles commitSha = do
   maybeCtx <- TaskCore.getRetryContext tid
@@ -219,17 +219,105 @@ handleConflict tid conflictFiles commitSha = do
       putText "[review] Task has failed 3 times. Needs human intervention."
       TaskCore.updateTaskStatus tid TaskCore.Open []
     else do
+      conflictDetails <- gatherConflictContext commitSha conflictFiles
       TaskCore.setRetryContext
         TaskCore.RetryContext
           { TaskCore.retryTaskId = tid,
             TaskCore.retryOriginalCommit = Text.pack commitSha,
             TaskCore.retryConflictFiles = conflictFiles,
             TaskCore.retryAttempt = attempt,
-            TaskCore.retryReason = "merge_conflict"
+            TaskCore.retryReason = conflictDetails
           }
       TaskCore.updateTaskStatus tid TaskCore.Open []
       putText ("[review] Task " <> tid <> " returned to queue (attempt " <> tshow attempt <> "/3).")
 
+-- | Gather Gerrit-style conflict context for the coder
+gatherConflictContext :: String -> [Text] -> IO Text
+gatherConflictContext commitSha conflictFiles = do
+  commitInfo <- getCommitInfo commitSha
+  currentHeadInfo <- getCurrentHeadInfo
+  fileDiffs <- traverse (getFileConflictInfo commitSha <. Text.unpack) conflictFiles
+
+  pure
+    <| Text.unlines
+      [ "MERGE CONFLICT - Your changes could not be cleanly applied",
+        "",
+        "== Your Commit ==",
+        commitInfo,
+        "",
+        "== Current HEAD ==",
+        currentHeadInfo,
+        "",
+        "== Conflicting Files ==",
+        Text.unlines fileDiffs,
+        "",
+        "== Resolution Instructions ==",
+        "1. The codebase has been updated since your work",
+        "2. Review the current state of conflicting files",
+        "3. Re-implement your changes on top of the current code",
+        "4. Ensure your changes still make sense given the updates"
+      ]
+
+-- | Get info about the commit that caused the conflict
+getCommitInfo :: String -> IO Text
+getCommitInfo sha = do
+  (_, out, _) <-
+    Process.readProcessWithExitCode
+      "git"
+      ["log", "-1", "--format=%h %s%n%b", sha]
+      ""
+  pure <| Text.pack out
+
+-- | Get info about current HEAD
+getCurrentHeadInfo :: IO Text
+getCurrentHeadInfo = do
+  (_, out, _) <-
+    Process.readProcessWithExitCode
+      "git"
+      ["log", "-1", "--format=%h %s (%cr)"]
+      ""
+  pure <| Text.pack out
+
+-- | Get file-level conflict context showing what changed in both branches
+getFileConflictInfo :: String -> FilePath -> IO Text
+getFileConflictInfo commitSha filePath = do
+  yourChanges <- getYourChangesToFile commitSha filePath
+  recentChanges <- getRecentChangesToFile filePath
+  pure
+    <| Text.unlines
+      [ "--- " <> Text.pack filePath <> " ---",
+        "",
+        "Your changes to this file:",
+        yourChanges,
+        "",
+        "Recent changes by others:",
+        recentChanges
+      ]
+
+-- | Get a summary of changes in a specific commit to a file
+getYourChangesToFile :: String -> FilePath -> IO Text
+getYourChangesToFile commitSha filePath = do
+  (code, out, _) <-
+    Process.readProcessWithExitCode
+      "git"
+      ["show", "--stat", commitSha, "--", filePath]
+      ""
+  case code of
+    Exit.ExitSuccess -> pure <| Text.pack (take 500 out)
+    Exit.ExitFailure _ -> pure "(unable to get diff)"
+
+-- | Get recent changes to a file (last few commits)
+getRecentChangesToFile :: FilePath -> IO Text
+getRecentChangesToFile filePath = do
+  (code, out, _) <-
+    Process.readProcessWithExitCode
+      "git"
+      ["log", "-3", "--oneline", "--", filePath]
+      ""
+  case code of
+    Exit.ExitSuccess -> pure <| Text.pack out
+    Exit.ExitFailure _ -> pure "(unable to get history)"
+
 -- | Interactive review command (jr review <task-id>)
 reviewTask :: Text -> Bool -> IO ()
 reviewTask tid autoMode = do