diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,20 @@
 
 ## [Unreleased]
 
+## 0.2.1.0 - 2026-08-17
+
+### Added
+
+- Tricorder now respects all forms of `cabal.project` files, like
+  `cabal.project.local` for example. See
+  [Cabal's documentation on project description files](https://cabal.readthedocs.io/en/stable/cabal-project-description-file.html). ([#73](https://github.com/tweag/tricorder/issues/73))
+- With `hpack` in `PATH`, Tricorder will now run `hpack` in the directory of
+  any changed `package.yaml` file in the root of the project or any
+  subdirectory. Control this behavior with the `generate_with_hpack`
+  configuration.
+- Tricorder will now restart the GHCi session when detecting changes to a
+  project's `stack.yaml`.
+
 ## [0.2.0.1] - 2026-08-12
 
 ### Fixed
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2025 Christian Georgii
+Copyright (c) 2025 Tweag
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/src/Tricorder/Daemon/Core.hs b/src/Tricorder/Daemon/Core.hs
--- a/src/Tricorder/Daemon/Core.hs
+++ b/src/Tricorder/Daemon/Core.hs
@@ -9,7 +9,7 @@
 import Atelier.Effects.FileWatcher (FileEvent, FileWatcher)
 import Atelier.Effects.Input (Input)
 import Atelier.Effects.Log (Log)
-import Atelier.Effects.Publishing (runPubSub_)
+import Atelier.Effects.Publishing (runPubSub)
 import Atelier.Effects.Publishing.Pub (Pub)
 import Atelier.Effects.Publishing.Sub (Sub)
 import Data.List (isSuffixOf)
@@ -18,8 +18,10 @@
 import Effectful.Reader.Static (Reader)
 import Effectful.State.Static.Shared (State)
 import Relude.Extra.Tuple (dup)
+import System.FilePath ((</>))
 
 import Atelier.Effects.Conc qualified as Conc
+import Atelier.Effects.FileSystem qualified as FileSystem
 import Atelier.Effects.FileWatcher qualified as FileWatcher
 import Atelier.Effects.Log qualified as Log
 import Atelier.Effects.Publishing.Pub qualified as Pub
@@ -52,12 +54,14 @@
     , LoadedModule (..)
     , resolveKnownTargets
     )
+import Tricorder.Daemon.Hpack.Effect (Hpack)
 import Tricorder.Daemon.TestRunner (TestRunner)
 import Tricorder.Daemon.Watch (WatchedFile)
 import Tricorder.Runtime (ProjectRoot (..))
 import Tricorder.Session (Session (..), loadSession)
 import Tricorder.Session.CabalFile (CabalFile)
 import Tricorder.Session.Command (Command (..))
+import Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..))
 import Tricorder.Session.TestTarget (TestTarget, renderTestTarget)
 import Tricorder.Session.TestTimeout (TestTimeout)
 import Tricorder.Waiters (Waiters)
@@ -68,6 +72,7 @@
 import Tricorder.Config qualified as Config
 import Tricorder.Daemon.Builder qualified as Builder
 import Tricorder.Daemon.EvalCommentRunner qualified as EvalCommentRunner
+import Tricorder.Daemon.Hpack qualified as Hpack
 import Tricorder.Daemon.TestRunner qualified as TestRunner
 import Tricorder.Daemon.Watch qualified as Watch
 import Tricorder.Waiters qualified as Waiters
@@ -90,6 +95,7 @@
        , FileSystem :> es
        , FileWatcher :> es
        , GhciSession :> es
+       , Hpack :> es
        , Input LoadedConfig :> es
        , Input [CabalFile] :> es
        , Log :> es
@@ -100,16 +106,17 @@
        , Waiters :> es
        )
     => Eff es Void
-main = runPubSub_ @ReloadSession
-    . runPubSub_ @WatchedFile
-    . runPubSub_ @CabalChangeDetected
-    . runPubSub_ @SourceChangeDetected
-    . runPubSub_ @RestartBuilder
-    . runPubSub_ @ReloadBuilder
+main = runPubSub @ReloadSession
+    . runPubSub @WatchedFile
+    . runPubSub @CabalChangeDetected
+    . runPubSub @SourceChangeDetected
+    . runPubSub @RestartBuilder
+    . runPubSub @ReloadBuilder
     $ Conc.restartableFork waitForReloadSession do
         root <- Reader.ask
         session <- loadSession
         Conc.fork_ $ watchConfigFile root
+        conditionallyWatchStackYaml root
 
         Conc.fork_ $ Watch.files root session
         Conc.fork_ $ Sub.listen_ Watch.publishChange
@@ -120,9 +127,13 @@
                 Pub.publish ReloadSession
             else
                 Pub.publish RestartBuilder
+
         Conc.fork_ $ Sub.listen_ \(SourceChangeDetected fp event) ->
             Pub.publish $ ReloadBuilder fp event
 
+        when session.generateWithHpack.getGenerateWithHpack do
+            void $ Conc.fork Hpack.main
+
         State.evalState emptyBuilderState $ withSession session
   where
     waitForReloadSession = Waiters.wait $ Sub.listenOnce_ @ReloadSession
@@ -150,7 +161,23 @@
 watchConfigFile root = do
     FileWatcher.watchFilePathsDebounced
         [FileWatcher.dirWhere root.getProjectRoot (Config.configFileName `isSuffixOf`)]
-        \_ _ -> Pub.publish $ ReloadSession
+        \_ _ -> Pub.publish ReloadSession
+
+
+conditionallyWatchStackYaml
+    :: ( Conc :> es
+       , Debounce FilePath :> es
+       , FileSystem :> es
+       , FileWatcher :> es
+       , Pub RestartBuilder :> es
+       )
+    => ProjectRoot -> Eff es ()
+conditionallyWatchStackYaml root = do
+    exists <- FileSystem.doesFileExist $ root.getProjectRoot </> "stack.yaml"
+    when exists do
+        Conc.fork_ $ FileWatcher.watchFilePathsDebounced
+            [FileWatcher.dirWhere root.getProjectRoot ("stack.yaml" `isSuffixOf`)]
+            \_ _ -> Pub.publish RestartBuilder
 
 
 -- | For a given session, handles controlling the build process itself,
diff --git a/src/Tricorder/Daemon/Hpack.hs b/src/Tricorder/Daemon/Hpack.hs
new file mode 100644
--- /dev/null
+++ b/src/Tricorder/Daemon/Hpack.hs
@@ -0,0 +1,81 @@
+module Tricorder.Daemon.Hpack (main) where
+
+import Atelier.Effects.Chan (Chan)
+import Atelier.Effects.Clock (Clock)
+import Atelier.Effects.Conc (Conc)
+import Atelier.Effects.Debounce (Debounce)
+import Atelier.Effects.FileWatcher (FileWatcher)
+import Atelier.Effects.Log (Log)
+import Atelier.Effects.Publishing (runPubSub)
+import Atelier.Effects.Publishing.Pub (Pub)
+import Effectful.Reader.Static (Reader)
+
+import Atelier.Effects.Conc qualified as Conc
+import Atelier.Effects.FileWatcher qualified as FileWatcher
+import Atelier.Effects.Log qualified as Log
+import Atelier.Effects.Publishing.Pub qualified as Pub
+import Atelier.Effects.Publishing.Sub qualified as Sub
+import Data.List qualified as List
+import Effectful.Reader.Static qualified as Reader
+
+import Tricorder.Daemon.Hpack.Effect (Hpack)
+import Tricorder.Runtime (ProjectRoot (..))
+
+import Tricorder.Daemon.Hpack.Effect qualified as Hpack
+
+
+main
+    :: ( Chan :> es
+       , Clock :> es
+       , Conc :> es
+       , Debounce FilePath :> es
+       , FileWatcher :> es
+       , Hpack :> es
+       , Log :> es
+       , Reader ProjectRoot :> es
+       )
+    => Eff es ()
+main = runPubSub @RePack $ Conc.scoped do
+    hpackInPath <- Hpack.hpackIsInPath
+    when hpackInPath do
+        projectRoot <- Reader.ask
+        Conc.fork_ $ Sub.listen_ \(RePack path) -> runHpack path
+        vacuous $ watchPackageYaml projectRoot
+
+
+runHpack :: (Hpack :> es, Log :> es) => FilePath -> Eff es ()
+runHpack path = do
+    res <- Hpack.hpack path
+    case res of
+        Left err -> Log.err $ "Hpack error: " <> err
+        Right result ->
+            Log.info
+                $ "Hpack: " <> case result of
+                    Hpack.Generated ->
+                        "generated " <> toText path
+                    Hpack.Unchanged ->
+                        "already up-to-date: " <> toText path
+                    Hpack.WasEditedManually ->
+                        "cabal file was edited manually: " <> toText path
+                    Hpack.WasGeneratedWithNewerHpack ->
+                        "cabal file was generated with a newer version of Hpack: " <> toText path
+                    Hpack.UnknownSuccess output -> "unknown successful output: " <> output
+
+
+watchPackageYaml
+    :: ( Debounce FilePath :> es
+       , FileWatcher :> es
+       , Pub RePack :> es
+       )
+    => ProjectRoot -> Eff es Void
+watchPackageYaml root = do
+    FileWatcher.watchFilePathsDebounced
+        [FileWatcher.dirWhere root.getProjectRoot (packageYaml `List.isSuffixOf`)]
+        \path _ -> Pub.publish $ RePack path
+
+
+data RePack = RePack FilePath
+
+
+packageYaml :: FilePath
+packageYaml = "package.yaml"
diff --git a/src/Tricorder/Daemon/Hpack/Effect.hs b/src/Tricorder/Daemon/Hpack/Effect.hs
new file mode 100644
--- /dev/null
+++ b/src/Tricorder/Daemon/Hpack/Effect.hs
@@ -0,0 +1,62 @@
+module Tricorder.Daemon.Hpack.Effect
+    ( Hpack (..)
+    , Result (..)
+    , hpackIsInPath
+    , hpack
+    , run
+    ) where
+
+import Atelier.Effects.Process (Process, readProcess, runProcess, setWorkingDir, shell)
+import Effectful (Effect)
+import Effectful.Dispatch.Dynamic (interpret_)
+import Effectful.Exception (trySync)
+import Effectful.TH (makeEffect)
+import System.Exit (ExitCode (..))
+
+import Data.ByteString.Char8 qualified as B8
+import System.FilePath qualified as Path
+
+
+data Hpack :: Effect where
+    HpackIsInPath :: Hpack m Bool
+    Hpack :: FilePath -> Hpack m (Either Text Result)
+
+
+data Result
+    = Generated
+    | Unchanged
+    | WasGeneratedWithNewerHpack
+    | WasEditedManually
+    | UnknownSuccess Text
+
+
+makeEffect ''Hpack
+
+
+run :: (Process :> es) => Eff (Hpack : es) a -> Eff es a
+run = interpret_ \case
+    HpackIsInPath -> do
+        exitCode <- runProcess $ shell "command -v hpack"
+        pure $ exitCode == ExitSuccess
+    Hpack path -> do
+        res <-
+            trySync
+                $ readProcess
+                $ setWorkingDir (Path.takeDirectory path)
+                $ shell "hpack"
+        case res of
+            Left ex -> pure $ Left $ show ex
+            Right (exitCode, lstdout, lstderr) -> do
+                let stdout = toStrict lstdout
+                    stderr = toStrict lstderr
+                    infoMsg =
+                        if
+                            | "generated" `B8.isInfixOf` stdout -> Generated
+                            | "is up-to-date" `B8.isInfixOf` stdout -> Unchanged
+                            | "generated with a newer version" `B8.isInfixOf` stdout -> WasGeneratedWithNewerHpack
+                            | "was modified manually" `B8.isInfixOf` stdout -> WasEditedManually
+                            | otherwise -> UnknownSuccess $ decodeUtf8 stdout
+                if exitCode == ExitSuccess then
+                    pure $ Right $ infoMsg
+                else
+                    pure $ Left $ decodeUtf8 stderr
diff --git a/src/Tricorder/Daemon/Main.hs b/src/Tricorder/Daemon/Main.hs
--- a/src/Tricorder/Daemon/Main.hs
+++ b/src/Tricorder/Daemon/Main.hs
@@ -12,7 +12,7 @@
 import Atelier.Effects.FileSystem (runFileSystemIO)
 import Atelier.Effects.FileWatcher (runFileWatcherIO)
 import Atelier.Effects.Process (runProcessIO)
-import Atelier.Effects.Publishing (runPubSub_)
+import Atelier.Effects.Publishing (runPubSub)
 import Atelier.Effects.Timeout (runTimeout)
 import Data.Default (def)
 import Effectful (runEff)
@@ -40,6 +40,7 @@
 import Tricorder.Daemon.Core qualified as Core
 import Tricorder.Daemon.DaemonInfo qualified as DaemonInfo
 import Tricorder.Daemon.EvalCommentRunner qualified as EvalCommentRunner
+import Tricorder.Daemon.Hpack.Effect qualified as Hpack
 import Tricorder.Daemon.TestRunner qualified as TestRunner
 import Tricorder.Socket.Server qualified as Server
 import Tricorder.SourceLookup qualified as SourceLookup
@@ -71,6 +72,7 @@
         . runLogging
         . inputLoadedConfig
         . runChan
+        . runEnv
         . inputCabalFiles
         . inputSession
         . runReader @CacheConfig.Config def
@@ -78,13 +80,13 @@
         . runCacheTtl @ModuleName @PackageId
         . runCacheTtl @(PackageId, SourceQuery) @SourceLookup.ModuleSourceResult
         . runProcessIO
-        . runEnv
         . runGhcPkgIO
         . runUnixSocketIO
         . runGhciSession
         . evalState (BuildId 1)
         . Input.fromState @BuildId
-        . runPubSub_ @BuildPhase
+        . runPubSub @BuildPhase
+        . Hpack.run
         . Hackage.run
         . PackageStore.run
         . EvalCommentRunner.run
diff --git a/src/Tricorder/Session.hs b/src/Tricorder/Session.hs
--- a/src/Tricorder/Session.hs
+++ b/src/Tricorder/Session.hs
@@ -18,6 +18,7 @@
 import Tricorder.Session.CabalFile (CabalFile)
 import Tricorder.Session.Command (Command (..), resolveCommand)
 import Tricorder.Session.Config (Config (..))
+import Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..))
 import Tricorder.Session.ReplBuildDir (ReplBuildDir (..))
 import Tricorder.Session.Target (Target, definesCustomPrelude, resolveTargets)
 import Tricorder.Session.TestTarget (TestTarget, resolveTestTargets)
@@ -34,6 +35,7 @@
     , watchExclusionPatterns :: WatchExclusionPatterns
     , replBuildDir :: ReplBuildDir
     , testTimeout :: TestTimeout
+    , generateWithHpack :: GenerateWithHpack
     }
     deriving stock (Eq)
 
@@ -48,6 +50,7 @@
             , watchExclusionPatterns = def
             , replBuildDir = def
             , testTimeout = def
+            , generateWithHpack = def
             }
 
 
@@ -101,6 +104,7 @@
             , testTargets
             , replBuildDir = ReplBuildDir cfgFile.replBuildDir
             , testTimeout = TestTimeout cfgFile.testTimeout
+            , generateWithHpack = GenerateWithHpack cfgFile.generateWithHpack
             }
 
 
diff --git a/src/Tricorder/Session/CabalFile.hs b/src/Tricorder/Session/CabalFile.hs
--- a/src/Tricorder/Session/CabalFile.hs
+++ b/src/Tricorder/Session/CabalFile.hs
@@ -4,6 +4,7 @@
     , discoverCabalFiles
     ) where
 
+import Atelier.Effects.Env (Env)
 import Atelier.Effects.FileSystem (FileSystem, doesFileExist, listDirectory, readFileBs)
 import Atelier.Effects.Input (Input, runInputEff)
 import Atelier.Effects.Log (Log)
@@ -14,6 +15,7 @@
 import Effectful.Reader.Static (Reader, ask)
 import System.FilePath (normalise, takeExtension, (</>))
 
+import Atelier.Effects.Env qualified as Env
 import Atelier.Effects.Log qualified as Log
 import Data.ByteString.Char8 qualified as BC
 import Data.Text qualified as T
@@ -29,14 +31,14 @@
 
 
 inputCabalFiles
-    :: ( FileSystem :> es
+    :: ( Env :> es
+       , FileSystem :> es
        , Log :> es
        , Reader ProjectRoot :> es
        )
     => Eff (Input [CabalFile] : es) a -> Eff es a
 inputCabalFiles = runInputEff do
-    projectRoot <- ask
-    projectFilePaths <- discoverCabalFiles projectRoot
+    projectFilePaths <- discoverCabalFiles
     (faileds, packageDescriptions) <-
         partitionEithers <$> for projectFilePaths \p -> do
             contents <- readFileBs p
@@ -50,38 +52,42 @@
     pure $ packageDescriptions
 
 
--- | Locate every package's @.cabal@ file, logging what drove the result. In a
--- multi-package project the packages live in subdirectories listed under
--- @packages:@ in @cabal.project@; in a single-package project the @.cabal@
--- file(s) sit in the root. Called once per session load; the result is shared
--- by target and watch-dir resolution.
-discoverCabalFiles :: (FileSystem :> es, Log :> es) => ProjectRoot -> Eff es [FilePath]
-discoverCabalFiles (ProjectRoot projectRoot) = do
-    hasProject <- doesFileExist projectFile
-    cabalFiles <-
-        if hasProject then do
-            contents <- readFileBs projectFile
-            concat <$> traverse cabalFilesForEntry (projectPackageEntries contents)
-        else
+-- | Lists all `.cabal` files for packages listed in the project root's
+-- `cabal.project` (or `cabal.project.local`) file. If no `cabal.project` file
+-- is found, looks for a `.cabal` file in the project root, and uses that
+-- instead.
+discoverCabalFiles
+    :: ( Env :> es
+       , FileSystem :> es
+       , Reader ProjectRoot :> es
+       )
+    => Eff es [FilePath]
+discoverCabalFiles = do
+    ProjectRoot projectRoot <- ask
+    homeCabalFiles <- maybe [] (one . (</> ".cabal/config")) <$> Env.lookupEnv "HOME"
+    let projectFilePaths = projectCabalFiles projectRoot <> homeCabalFiles
+    projectFiles <- filterM doesFileExist projectFilePaths
+    case nonEmpty projectFiles of
+        Nothing ->
             cabalFilesIn projectRoot
-    let listed
-            | null cabalFiles = "none"
-            | otherwise = T.intercalate ", " (map toText cabalFiles)
-    if hasProject then
-        Log.debug
-            $ "Found cabal.project; discovered "
-                <> show (length cabalFiles)
-                <> " package cabal file(s): "
-                <> listed
-    else
-        Log.debug $ "No cabal.project; using cabal file(s) in project root: " <> listed
-    pure cabalFiles
+        Just neProjectFiles -> do
+            packages <- fmap (find (not . null))
+                $ for (toList neProjectFiles) \projectFile -> do
+                    contents <- readFileBs projectFile
+                    concat
+                        <$> traverse
+                            (cabalFilesForEntry projectRoot)
+                            (projectPackageEntries contents)
+            case packages of
+                Nothing -> cabalFilesIn projectRoot
+                Just pkgs -> pure pkgs
   where
-    projectFile = projectRoot </> "cabal.project"
+    projectCabalFiles projectRoot =
+        (projectRoot </>) <$> ["cabal.project.local", "cabal.project.freeze", "cabal.project"]
 
     -- A @packages:@ entry is either a direct path to a @.cabal@ file or a
     -- directory to search for one.
-    cabalFilesForEntry entry
+    cabalFilesForEntry projectRoot entry
         | takeExtension entry == ".cabal" = pure [projectRoot </> entry]
         | otherwise = cabalFilesIn (normalise (projectRoot </> entry))
 
diff --git a/src/Tricorder/Session/Config.hs b/src/Tricorder/Session/Config.hs
--- a/src/Tricorder/Session/Config.hs
+++ b/src/Tricorder/Session/Config.hs
@@ -14,6 +14,7 @@
     , testTargets :: Maybe [Text]
     , replBuildDir :: FilePath
     , testTimeout :: Int
+    , generateWithHpack :: Bool
     }
     deriving stock (Eq, Generic, Show)
     deriving (FromJSON) via WithDefaults (QuietSnake Config)
@@ -29,4 +30,5 @@
             , testTargets = Nothing
             , replBuildDir = "dist-newstyle/tricorder"
             , testTimeout = 10
+            , generateWithHpack = True
             }
diff --git a/src/Tricorder/Session/GenerateWithHpack.hs b/src/Tricorder/Session/GenerateWithHpack.hs
new file mode 100644
--- /dev/null
+++ b/src/Tricorder/Session/GenerateWithHpack.hs
@@ -0,0 +1,13 @@
+module Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..)) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Default (Default (..))
+
+
+newtype GenerateWithHpack = GenerateWithHpack {getGenerateWithHpack :: Bool}
+    deriving stock (Eq, Generic, Show)
+    deriving (FromJSON, ToJSON) via Bool
+
+
+instance Default GenerateWithHpack where
+    def = GenerateWithHpack True
diff --git a/test/Unit/Tricorder/Session/CabalFileSpec.hs b/test/Unit/Tricorder/Session/CabalFileSpec.hs
--- a/test/Unit/Tricorder/Session/CabalFileSpec.hs
+++ b/test/Unit/Tricorder/Session/CabalFileSpec.hs
@@ -1,8 +1,9 @@
 module Unit.Tricorder.Session.CabalFileSpec (spec_CabalFile) where
 
+import Atelier.Effects.Env (runEnvConst)
 import Atelier.Effects.FileSystem (runFileSystemState)
-import Atelier.Effects.Log (runLogNoOp)
 import Effectful (runPureEff)
+import Effectful.Reader.Static (runReader)
 import Effectful.State.Static.Shared (evalState)
 import Test.Hspec (Spec, describe, it, shouldBe)
 
@@ -10,7 +11,7 @@
 
 import Tricorder.Runtime (ProjectRoot (..))
 import Tricorder.Session.CabalFile (discoverCabalFiles)
-import Unit.Tricorder.Session.Helpers (cabalFixture, multiPackageFs)
+import Unit.Tricorder.Session.Helpers (cabalFixture, libTestCabal, multiPackageFs)
 
 
 spec_CabalFile :: Spec
@@ -18,26 +19,100 @@
     describe "discoverCabalFiles" testDiscoverCabalFiles
 
 
--- | Pins the discovery contract: a @cabal.project@ selects per-package
--- @.cabal@ files from its @packages:@ stanza; otherwise the @.cabal@ files in
--- the project root are used.
+-- | Pins the discovery contract: a @cabal.project@ (or @.local@/@.freeze@
+-- variant) selects per-package @.cabal@ files from its @packages:@ stanza;
+-- otherwise the @.cabal@ files in the project root are used. Falls back
+-- further to @$HOME/.cabal/config@'s @packages:@ stanza if none of the
+-- project-root files exist.
 testDiscoverCabalFiles :: Spec
 testDiscoverCabalFiles = do
     describe "when there is no cabal.project" do
         it "finds the .cabal files in the project root" do
             let actual =
-                    runDiscovery (Map.singleton "/myapp.cabal" cabalFixture)
-                        $ discoverCabalFiles pr
+                    runDiscovery (Map.singleton "/myapp.cabal" cabalFixture) []
+                        $ discoverCabalFiles
             actual `shouldBe` ["/myapp.cabal"]
 
         it "returns no files when the root has no cabal file" do
-            let actual = runDiscovery mempty $ discoverCabalFiles pr
+            let actual = runDiscovery mempty [] discoverCabalFiles
             actual `shouldBe` []
 
     describe "when there is a multi-package cabal.project" do
         it "resolves each listed package to its .cabal (regression: was root-only)" do
-            let actual = runDiscovery multiPackageFs $ discoverCabalFiles pr
+            let actual = runDiscovery multiPackageFs [] discoverCabalFiles
             actual `shouldBe` ["/pkg-a/pkg-a.cabal", "/pkg-b/pkg-b.cabal"]
+
+    describe "priority among cabal.project.local, cabal.project.freeze, and cabal.project" do
+        it "prefers cabal.project.local over cabal.project" do
+            let fs =
+                    Map.fromList
+                        [ ("/cabal.project.local", "packages: pkg-a\n")
+                        , ("/cabal.project", "packages: pkg-b\n")
+                        ]
+                        `Map.union` multiPackageCabalFiles
+                actual = runDiscovery fs [] discoverCabalFiles
+            actual `shouldBe` ["/pkg-a/pkg-a.cabal"]
+
+        it "prefers cabal.project.freeze over cabal.project" do
+            let fs =
+                    Map.fromList
+                        [ ("/cabal.project.freeze", "packages: pkg-a\n")
+                        , ("/cabal.project", "packages: pkg-b\n")
+                        ]
+                        `Map.union` multiPackageCabalFiles
+                actual = runDiscovery fs [] discoverCabalFiles
+            actual `shouldBe` ["/pkg-a/pkg-a.cabal"]
+
+        describe "when a higher-priority file lists no packages" do
+            it "falls through to the next file in priority order" do
+                let fs =
+                        Map.fromList
+                            [ ("/cabal.project.local", "tests: True\n")
+                            , ("/cabal.project", "packages: pkg-b\n")
+                            ]
+                            `Map.union` multiPackageCabalFiles
+                    actual = runDiscovery fs [] discoverCabalFiles
+                actual `shouldBe` ["/pkg-b/pkg-b.cabal"]
+
+    describe "packages: entry resolution" do
+        it "uses a direct .cabal path entry verbatim, without scanning a directory" do
+            let fs = Map.singleton "/cabal.project" "packages: sub/foo.cabal\n"
+                actual = runDiscovery fs [] discoverCabalFiles
+            actual `shouldBe` ["/sub/foo.cabal"]
+
+        it "skips glob entries under packages: (not expanded)" do
+            let fs = Map.singleton "/cabal.project" "packages: */*.cabal\n"
+                actual = runDiscovery fs [] discoverCabalFiles
+            actual `shouldBe` []
+
+    describe "$HOME/.cabal/config fallback" do
+        describe "when no cabal.project files exist" do
+            it "uses $HOME/.cabal/config as a last-resort packages source" do
+                let fs =
+                        Map.singleton "/home/user/.cabal/config" "packages: pkg-a\n"
+                            `Map.union` multiPackageCabalFiles
+                    actual = runDiscovery fs [("HOME", "/home/user")] discoverCabalFiles
+                actual `shouldBe` ["/pkg-a/pkg-a.cabal"]
+
+        describe "when $HOME/.cabal/config exists but lists no packages" do
+            it "falls back to scanning the project root" do
+                let fs =
+                        Map.fromList
+                            [ ("/home/user/.cabal/config", "")
+                            , ("/myapp.cabal", cabalFixture)
+                            ]
+                    actual = runDiscovery fs [("HOME", "/home/user")] discoverCabalFiles
+                actual `shouldBe` ["/myapp.cabal"]
   where
     pr = ProjectRoot "/"
-    runDiscovery fs = runPureEff . evalState fs . runFileSystemState . runLogNoOp
+    multiPackageCabalFiles =
+        Map.fromList
+            [ ("/pkg-a/pkg-a.cabal", libTestCabal "pkg-a")
+            , ("/pkg-b/pkg-b.cabal", libTestCabal "pkg-b")
+            ]
+    runDiscovery fs env =
+        runPureEff
+            . runEnvConst env
+            . evalState fs
+            . runFileSystemState
+            . runReader pr
diff --git a/tricorder.cabal b/tricorder.cabal
--- a/tricorder.cabal
+++ b/tricorder.cabal
@@ -4,18 +4,18 @@
 --
 -- see: https://github.com/sol/hpack
 
-name:           tricorder
-version:        0.2.0.1
-synopsis:       Continuous Haskell build status, diagnostics, and tests via a shared daemon
-description:    tricorder rebuilds your Haskell project continuously and surfaces build status, diagnostics, test results, and documentation - for developers and LLM coding agents. Like ghcid and ghciwatch it reloads on every change, but builds run in a background daemon so multiple clients (an interactive TUI, a status CLI, an agent skill) share a single build state without triggering redundant rebuilds. It discovers components across multi-package cabal.project workspaces automatically and ships context-friendly output for agentic use via the CLI.
-category:       Development
-homepage:       https://github.com/tweag/tricorder#readme
-bug-reports:    https://github.com/tweag/tricorder/issues
-author:         Christian Georgii
-maintainer:     christian.georgii@tweag.io
-license:        MIT
-license-file:   LICENSE
-build-type:     Simple
+name:            tricorder
+version:         0.2.1.0
+synopsis:        Continuous Haskell build status, diagnostics, and tests via a shared daemon
+description:     tricorder rebuilds your Haskell project continuously and surfaces build status, diagnostics, test results, and documentation - for developers and LLM coding agents. Like ghcid and ghciwatch it reloads on every change, but builds run in a background daemon so multiple clients (an interactive TUI, a status CLI, an agent skill) share a single build state without triggering redundant rebuilds. It discovers components across multi-package cabal.project workspaces automatically and ships context-friendly output for agentic use via the CLI.
+category:        Development
+homepage:        https://github.com/tweag/tricorder#readme
+bug-reports:     https://github.com/tweag/tricorder/issues
+author:          Victor Nascimento Bakke
+maintainer:      victor.bakke@tweag.io
+license:         MIT
+license-file:    LICENSE
+build-type:      Simple
 extra-doc-files:
     README.md
     CHANGELOG.md
@@ -54,6 +54,8 @@
       Tricorder.Daemon.GhciSession
       Tricorder.Daemon.GhciSession.GhciParser
       Tricorder.Daemon.GhciSession.GhciProcess
+      Tricorder.Daemon.Hpack
+      Tricorder.Daemon.Hpack.Effect
       Tricorder.Daemon.Main
       Tricorder.Daemon.TestRunner
       Tricorder.Daemon.Watch
@@ -64,6 +66,7 @@
       Tricorder.Session.CabalFile
       Tricorder.Session.Command
       Tricorder.Session.Config
+      Tricorder.Session.GenerateWithHpack
       Tricorder.Session.ReplBuildDir
       Tricorder.Session.Target
       Tricorder.Session.TestTarget
@@ -108,14 +111,13 @@
       TypeFamilies
   ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded
   build-depends:
-      Cabal >=3.12 && <3.17
-    , Cabal-syntax >=3.12 && <3.17
-    , aeson ==2.2.*
-    , ansi-terminal ==1.1.*
-    , atelier-core ==0.3.*
+      Cabal >=3.12 && <3.19
+    , Cabal-syntax >=3.12 && <3.19
+    , aeson >=2.2 && <2.4
+    , atelier-core >=0.3 && <0.5
     , atelier-prelude >=0.1 && <0.3
     , base >=4.18 && <4.23
-    , brick ==2.10.*
+    , brick >=2.10 && <2.14
     , bytestring >=0.11 && <0.13
     , casing ==0.1.*
     , containers >=0.6 && <0.9
@@ -127,7 +129,7 @@
     , effectful-th ==1.0.*
     , filepath >=1.4 && <1.6
     , hashable ==1.5.*
-    , megaparsec ==9.7.*
+    , megaparsec >=9.7 && <9.9
     , mtl ==2.3.*
     , network ==3.2.*
     , optparse-applicative ==0.19.*
@@ -139,9 +141,9 @@
     , tar >=0.6 && <0.8
     , template-haskell >=2.20 && <2.25
     , text ==2.1.*
-    , time >=1.12 && <1.16
+    , time >=1.12 && <1.17
     , time-units ==1.0.*
-    , vty ==6.5.*
+    , vty >=6.5 && <6.7
     , vty-crossplatform ==0.5.*
     , yaml ==0.11.*
     , zlib ==0.7.*
@@ -281,9 +283,9 @@
   build-tool-depends:
       tasty-discover:tasty-discover
   build-depends:
-      Cabal-syntax >=3.12 && <3.17
-    , aeson ==2.2.*
-    , atelier-core ==0.3.*
+      Cabal-syntax >=3.12 && <3.19
+    , aeson >=2.2 && <2.4
+    , atelier-core >=0.3 && <0.5
     , atelier-prelude >=0.1 && <0.3
     , base >=4.18 && <4.23
     , bytestring >=0.11 && <0.13
@@ -294,7 +296,7 @@
     , effectful-plugin >=2.0 && <2.2
     , filepath >=1.4 && <1.6
     , hspec ==2.11.*
-    , megaparsec ==9.7.*
+    , megaparsec >=9.7 && <9.9
     , process ==1.6.*
     , regex-tdfa >=1.3.2.5 && <1.4
     , stm ==2.5.*
@@ -303,7 +305,7 @@
     , tasty-discover ==5.2.*
     , tasty-hspec ==1.2.*
     , text ==2.1.*
-    , time >=1.12 && <1.16
+    , time >=1.12 && <1.17
     , time-units ==1.0.*
     , tricorder-internal
     , typed-process ==0.2.*
