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