← Back to task

Commit d90ccfba

commit d90ccfba7c7f25a97e3673d4218c79bf901b6542
Author: Coder Agent <coder@agents.omni>
Date:   Thu Feb 19 16:54:09 2026

    Pipeline: verify changed Haskell targets from git diff
    
    Task-Id: t-656

diff --git a/Omni/Pipeline.hs b/Omni/Pipeline.hs
index 05e9187a..dbb5ed6a 100755
--- a/Omni/Pipeline.hs
+++ b/Omni/Pipeline.hs
@@ -404,7 +404,6 @@ doIntegrate cfg db pool task = do
   let tid = Task.taskId task
       baseBranch = Core.pcBaseBranch cfg
       repoRoot = Core.pcRepoRoot cfg
-      ns = Task.taskNamespace task
   now <- getCurrentTime
 
   -- Record the integration attempt
@@ -424,7 +423,7 @@ doIntegrate cfg db pool task = do
           Core.rrError = Nothing
         }
 
-  result <- Integrate.integrateTask pool repoRoot tid baseBranch ns
+  result <- Integrate.integrateTask pool repoRoot tid baseBranch
   case result of
     Core.Integrated commitHash -> do
       State.markRunFinished db rowId Core.Success Nothing Nothing
@@ -456,7 +455,6 @@ doVerify cfg db pool ad = do
   let tid = Core.adTaskId ad
       baseBranch = Core.pcBaseBranch cfg
       workspace = Core.adWorkspace ad
-      ns = Core.adNamespace ad
   now <- getCurrentTime
 
   rowId <-
@@ -475,7 +473,7 @@ doVerify cfg db pool ad = do
           Core.rrError = Nothing
         }
 
-  result <- Verify.verifyTask workspace tid baseBranch ns
+  result <- Verify.verifyTask workspace tid baseBranch
   case result of
     Core.VerifyPass -> do
       State.markRunFinished db rowId Core.Success Nothing Nothing
diff --git a/Omni/Pipeline/Git.hs b/Omni/Pipeline/Git.hs
index ef16fd82..ad97db7d 100644
--- a/Omni/Pipeline/Git.hs
+++ b/Omni/Pipeline/Git.hs
@@ -96,6 +96,19 @@ commitCountBetween workspace base headRef = do
       [(n, _)] -> pure (Right n)
       _ -> pure (Left (GitError "rev-list --count parse" 1 ("Could not parse: " <> T.pack s)))
 
+-- | List files changed between two refs: `git diff --name-only base..head`.
+changedFiles :: FilePath -> Text -> Text -> IO (Either GitError [FilePath])
+changedFiles workspace base headRef = do
+  result <- runGit workspace ["diff", "--name-only", T.unpack base <> ".." <> T.unpack headRef]
+  case result of
+    Left e -> pure (Left e)
+    Right out ->
+      pure
+        <| Right
+          <| map T.unpack
+          <| filter (not <. T.null)
+          <| T.lines (T.pack out)
+
 -- | Get the SHA of a branch tip, or Nothing if it doesn't exist.
 branchSha :: FilePath -> Text -> IO (Maybe Text)
 branchSha workspace ref = do
diff --git a/Omni/Pipeline/Integrate.hs b/Omni/Pipeline/Integrate.hs
index a6199369..3d1ef73e 100644
--- a/Omni/Pipeline/Integrate.hs
+++ b/Omni/Pipeline/Integrate.hs
@@ -23,9 +23,8 @@ integrateTask ::
   FilePath ->
   Text ->
   Text ->
-  Maybe Text ->
   IO Core.IntegrateResult
-integrateTask pool repoRoot taskId baseBranch maybeNamespace = do
+integrateTask pool repoRoot taskId baseBranch = do
   -- 1. Ensure integration worktree
   wsResult <- Workspace.ensureIntegrationWorktree pool
   case wsResult of
@@ -55,8 +54,8 @@ integrateTask pool repoRoot taskId baseBranch maybeNamespace = do
                     )
                 )
             Right commitHash -> do
-              -- 3. Verify build on base branch
-              verifyResult <- Verify.verifyOnBase workspace maybeNamespace
+              -- 3. Verify build on base branch (using changed-file targets)
+              verifyResult <- Verify.verifyOnBase workspace baseBranch
               case verifyResult of
                 Core.VerifyFail reason -> do
                   -- Revert the cherry-pick
diff --git a/Omni/Pipeline/Verify.hs b/Omni/Pipeline/Verify.hs
index d23084e0..d5b535f7 100644
--- a/Omni/Pipeline/Verify.hs
+++ b/Omni/Pipeline/Verify.hs
@@ -7,6 +7,7 @@
 module Omni.Pipeline.Verify where
 
 import Alpha
+import qualified Data.List as List
 import qualified Data.Text as T
 import qualified Omni.Pipeline.Core as Core
 import qualified Omni.Pipeline.Git as Git
@@ -22,11 +23,19 @@ data BildResult
   | BildNotBuildable -- exit code 2
   deriving (Show, Eq)
 
--- | Verify a task: check branch shape, run bild, run tests.
+-- | Per-target verification result.
+data TargetResult
+  = TargetPass
+  | TargetFail Text
+  | TargetNotBuildable
+  deriving (Show, Eq)
+
+-- | Verify a task: check branch shape, resolve changed targets via git diff,
+-- then run bild + tests on each affected target.
 --
 -- Runs in the dev worktree where the commit already exists.
-verifyTask :: FilePath -> Text -> Text -> Maybe Text -> IO Core.VerifyResult
-verifyTask workspace taskId baseBranch maybeNamespace = do
+verifyTask :: FilePath -> Text -> Text -> IO Core.VerifyResult
+verifyTask workspace taskId baseBranch = do
   -- 1. Check branch shape: exactly 1 commit between base and task
   countResult <- Git.commitCountBetween workspace baseBranch taskId
   case countResult of
@@ -44,81 +53,108 @@ verifyTask workspace taskId baseBranch maybeNamespace = do
                     <> T.pack (show n)
                 )
             )
-      | otherwise -> do
-          -- 2. Run build verification
-          case maybeNamespace of
-            Nothing -> pure (Core.VerifySkip "No namespace set, skipping build verification")
-            Just ns -> do
-              buildResult <- runBild workspace ns
-              case buildResult of
-                BildNotBuildable ->
-                  pure (Core.VerifySkip ("Namespace " <> ns <> " is not buildable (bild exit 2)"))
-                BildFail code output ->
-                  pure
-                    ( Core.VerifyFail
-                        ( "Build failed for "
-                            <> ns
-                            <> " (exit "
-                            <> T.pack (show code)
-                            <> "):\n"
-                            <> output
-                        )
-                    )
-                BildPass -> do
-                  -- 3. Run tests
-                  testResult <- runBildTest workspace ns
-                  case testResult of
-                    BildNotBuildable -> pure Core.VerifyPass -- no tests to run
-                    BildFail code output ->
-                      pure
-                        ( Core.VerifyFail
-                            ( "Tests failed for "
-                                <> ns
-                                <> " (exit "
-                                <> T.pack (show code)
-                                <> "):\n"
-                                <> output
-                            )
-                        )
-                    BildPass -> pure Core.VerifyPass
+      | otherwise -> verifyDiffRange workspace baseBranch taskId
 
 -- | Post-integration verification on the base branch.
--- Just runs bild + tests, no branch shape check.
-verifyOnBase :: FilePath -> Maybe Text -> IO Core.VerifyResult
-verifyOnBase workspace maybeNamespace =
-  case maybeNamespace of
-    Nothing -> pure (Core.VerifySkip "No namespace, skipping post-integration verify")
-    Just ns -> do
-      buildResult <- runBild workspace ns
-      case buildResult of
-        BildNotBuildable -> pure (Core.VerifySkip ("Not buildable: " <> ns))
-        BildFail code output ->
-          pure
-            ( Core.VerifyFail
-                ( "Post-integration build failed for "
-                    <> ns
-                    <> " (exit "
-                    <> T.pack (show code)
-                    <> "):\n"
-                    <> output
-                )
+--
+-- Determines targets from files changed between baseBranch and HEAD in the
+-- integration worktree.
+verifyOnBase :: FilePath -> Text -> IO Core.VerifyResult
+verifyOnBase workspace baseBranch =
+  verifyDiffRange workspace baseBranch "HEAD"
+
+-- | Resolve changed files from git diff and verify all affected Haskell targets.
+verifyDiffRange :: FilePath -> Text -> Text -> IO Core.VerifyResult
+verifyDiffRange workspace baseRef headRef = do
+  changedResult <- Git.changedFiles workspace baseRef headRef
+  case changedResult of
+    Left e ->
+      pure
+        ( Core.VerifyFail
+            ( "Failed to list changed files for "
+                <> diffRange baseRef headRef
+                <> ": "
+                <> Git.geStderr e
             )
-        BildPass -> do
-          testResult <- runBildTest workspace ns
-          case testResult of
-            BildNotBuildable -> pure Core.VerifyPass
-            BildFail code output ->
+        )
+    Right changedFiles
+      | null changedFiles ->
+          pure (Core.VerifySkip ("No files changed in " <> diffRange baseRef headRef <> ", skipping verification"))
+      | otherwise -> do
+          let targets = targetsFromChangedFiles changedFiles
+          if null targets
+            then
               pure
-                ( Core.VerifyFail
-                    ( "Post-integration tests failed for "
-                        <> ns
-                        <> " (exit "
-                        <> T.pack (show code)
-                        <> "):\n"
-                        <> output
+                ( Core.VerifySkip
+                    ( "No changed Haskell targets in "
+                        <> diffRange baseRef headRef
+                        <> ", skipping verification"
                     )
                 )
-            BildPass -> pure Core.VerifyPass
+            else verifyTargets workspace targets
+
+-- | Convert changed file paths into potential bild targets.
+-- Each changed .hs file is treated as a potential target.
+targetsFromChangedFiles :: [FilePath] -> [Text]
+targetsFromChangedFiles changedFiles =
+  List.nub <| map T.pack <| filter (List.isSuffixOf ".hs") changedFiles
+
+-- | Run build+test verification on all targets and aggregate failures.
+verifyTargets :: FilePath -> [Text] -> IO Core.VerifyResult
+verifyTargets workspace targets = do
+  results <- mapM (verifyTarget workspace) targets
+  let failures = [msg | TargetFail msg <- results]
+      buildableCount = length [() | r <- results, r /= TargetNotBuildable]
+  if not (null failures)
+    then pure (Core.VerifyFail (renderFailures failures))
+    else
+      if buildableCount == 0
+        then pure (Core.VerifySkip "Changed Haskell files are not buildable targets (bild exit 2), skipping verification")
+        else pure Core.VerifyPass
+
+-- | Verify one target with `bild` then `bild --test`.
+verifyTarget :: FilePath -> Text -> IO TargetResult
+verifyTarget workspace target = do
+  buildResult <- runBild workspace target
+  case buildResult of
+    BildNotBuildable -> pure TargetNotBuildable
+    BildFail code output ->
+      pure
+        <| TargetFail
+          ( "Build failed for "
+              <> target
+              <> " (exit "
+              <> T.pack (show code)
+              <> "):\n"
+              <> output
+          )
+    BildPass -> do
+      testResult <- runBildTest workspace target
+      case testResult of
+        BildNotBuildable -> pure TargetPass -- no tests to run
+        BildFail code output ->
+          pure
+            <| TargetFail
+              ( "Tests failed for "
+                  <> target
+                  <> " (exit "
+                  <> T.pack (show code)
+                  <> "):\n"
+                  <> output
+              )
+        BildPass -> pure TargetPass
+
+-- | Render multiple target failures into one summary.
+renderFailures :: [Text] -> Text
+renderFailures failures =
+  "Verification failed for "
+    <> T.pack (show (length failures))
+    <> " target(s):\n\n"
+    <> T.intercalate "\n\n---\n\n" failures
+
+-- | Human-readable diff range text.
+diffRange :: Text -> Text -> Text
+diffRange baseRef headRef = baseRef <> ".." <> headRef
 
 -- | Run `bild <namespace>` and interpret the exit code.
 runBild :: FilePath -> Text -> IO BildResult