diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,37 @@
 
 ## [Unreleased]
 
+## [1.0.0.0] - 2026-10-29
+
+### Added
+
+- Flag `--use-current` to force Tricorder to _not_ wait for the current build,
+  and use the incomplete build when reporting the status. This is the inverse
+  of the previous `--wait` flag.
+- Flag `--prevent-daemon` to prevent Tricorder from starting the daemon if it
+  is not already running. This is the inverse of the previous `--start-daemon`
+  flag.
+
+### Changed
+
+- Moved config option `session.test_timeout` to `session.test.timeout_ms`.
+- Moved config option `session.test_memory_limit` to `session.test.memory_limit`.
+- Renamed flag `--force` to `--ignore-pending-queries`.
+
+### Removed
+
+- Removed deprecated config option `session.command`.
+- Removed deprecated config option `session.targets`.
+- Removed deprecated config option `session.test_targets`.
+- Removed deprecated subcommand `test-results`.
+- Removed deprecated subcommand `start`.
+- Removed deprecated subcommand `stop`.
+- Removed deprecated subcommand `restart`.
+- Removed flag `--wait`. Tricorder now waits for the current build to finish by
+  default. Use `--use-current` to keep the old default behavior.
+- Removed flag `--start-daemon`. Tricorder now starts the daemon by default.
+  Use `--prevent-daemon` to keep the old default behavior.
+
 ## [0.7.0.0] - 2026-10-23
 
 ### Added
diff --git a/src/Tricorder/Build/Changes.hs b/src/Tricorder/Build/Changes.hs
--- a/src/Tricorder/Build/Changes.hs
+++ b/src/Tricorder/Build/Changes.hs
@@ -5,7 +5,7 @@
     )
 where
 
-import Atelier.Effects.FileWatcher (FileEvent)
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher (Event)
 
 
 -- | Classifies what kind of file change triggered a dirty signal.
@@ -14,9 +14,9 @@
 data ChangeKind = SourceChange | CabalChange deriving stock (Eq, Ord, Show)
 
 
-data CabalChangeDetected = CabalChangeDetected FilePath FileEvent
+data CabalChangeDetected = CabalChangeDetected FilePath FileWatcher.Event
     deriving stock (Eq, Show)
 
 
-data SourceChangeDetected = SourceChangeDetected FilePath FileEvent
+data SourceChangeDetected = SourceChangeDetected FilePath FileWatcher.Event
     deriving stock (Eq, Show)
diff --git a/src/Tricorder/CLI/App.hs b/src/Tricorder/CLI/App.hs
--- a/src/Tricorder/CLI/App.hs
+++ b/src/Tricorder/CLI/App.hs
@@ -28,14 +28,13 @@
 import Tricorder.API.IfDaemonIsStopped qualified as IfDaemonIsStopped
 
 import Tricorder.CLI.Daemon (startDaemon, waitForDaemon)
-import Tricorder.CLI.Operations (showEvalComments, showLog, showSource, showStatus, showTests)
+import Tricorder.CLI.Operations (showEvalComments, showLog, showSource, showStatus)
 import Tricorder.CLI.UI (viewUi)
 import Tricorder.CLI.UI.Brick (Brick)
 import Tricorder.CLI.UI.BrickChan (BrickChan)
 import Tricorder.Daemon.DaemonInfo (DaemonInfo)
 import Tricorder.Runtime (LogPath (..), PidFile (..), SocketPath (..))
 import Tricorder.Session.CabalFile (CabalFile)
-import Tricorder.Session.Repl (Repl)
 import Tricorder.Socket.Client (isDaemonRunning)
 import Tricorder.Socket.UnixSocket (UnixSocket)
 import Tricorder.SourceLookup.GhcPkg (GhcPkg)
@@ -45,6 +44,7 @@
 import Tricorder.CLI.App.Daemon qualified as Daemon
 import Tricorder.CLI.App.Test qualified as Test
 import Tricorder.CLI.UI.Keys qualified as Keys
+import Tricorder.Session.Stage.Build.Session qualified as Build (Session)
 
 
 run
@@ -62,8 +62,8 @@
        , GhcPkg :> es
        , Hackage :> es
        , IOE :> es
+       , Input Build.Session :> es
        , Input DaemonInfo :> es
-       , Input Repl :> es
        , Input [CabalFile] :> es
        , Log :> es
        , PackageStore :> es
@@ -89,11 +89,6 @@
                     showStatus opts
                 | not running -> Console.putStrLn "Stopped."
                 | otherwise -> showStatus opts
-        TestResults opts -> do
-            running <- isDaemonRunning
-            if not running
-                then Console.putStrLn "Stopped."
-                else showTests opts
         Log logMode -> do
             logFile <- asks @LogPath (.getLogPath)
             case logMode of
diff --git a/src/Tricorder/CLI/App/Test.hs b/src/Tricorder/CLI/App/Test.hs
--- a/src/Tricorder/CLI/App/Test.hs
+++ b/src/Tricorder/CLI/App/Test.hs
@@ -2,19 +2,21 @@
 
 import Atelier.Effects.Console (Console)
 import Atelier.Effects.Delay (Delay)
-import Atelier.Effects.Exit (Exit, exitFailure)
+import Atelier.Effects.Exit (Exit)
 import Atelier.Effects.File (File)
 import Atelier.Effects.Posix.Daemons (Daemons, PidFile)
 import Effectful (IOE)
 import Effectful.Reader.Static (Reader)
+import Tricorder.API.IfBuildIsPending (IfBuildIsPending (..))
 import Tricorder.API.IfDaemonIsStopped (IfDaemonIsStopped)
 import Tricorder.API.OutputFormat (OutputFormat (..))
 import Tricorder.API.Test (Command (..), SuitesOptions (..))
-import Tricorder.API.WaitMode (WaitMode (..))
 
 import Atelier.Effects.Console qualified as Console
+import Atelier.Effects.Exit qualified as Exit
 import Effectful.Reader.Static qualified as Reader
 import Tricorder.API.FieldMask.Schema qualified as FieldMask.Schema
+import Tricorder.API.IfBuildIsPending qualified as IfBuildIsPending
 import Tricorder.API.IfDaemonIsStopped qualified as IfDaemonIsStopped
 import Tricorder.API.Test.SuiteView.Schema qualified as SuiteView.Schema
 
@@ -51,7 +53,7 @@
 printResult = \case
     Left err -> do
         Console.putTextLn err
-        exitFailure
+        Exit.failure
     Right res ->
         Console.putTextLn res
 
@@ -73,7 +75,7 @@
             JsonOutput -> Json.argumentError err
         Right (targets, selection) ->
             ensureDaemonRunning opts.ifDaemonIsStopped do
-                result <- awaitBuildStatus opts.wait
+                result <- awaitBuildStatus opts.ifBuildIsPending
                 case opts.format of
                     TextOutput ->
                         pure
@@ -106,12 +108,12 @@
        , Reader SocketPath :> es
        , UnixSocket :> es
        )
-    => WaitMode -> Eff es (Either Text BuildState)
+    => IfBuildIsPending -> Eff es (Either Text BuildState)
 awaitBuildStatus wait = do
     SocketPath sockPath <- Reader.ask
     case wait of
-        WaitForBuild -> queryStatusWait sockPath
-        ShowCurrent -> queryStatus sockPath
+        IfBuildIsPending.WaitForBuild -> queryStatusWait sockPath
+        IfBuildIsPending.UseCurrent -> queryStatus sockPath
 
 
 ensureDaemonRunning
diff --git a/src/Tricorder/CLI/Arguments.hs b/src/Tricorder/CLI/Arguments.hs
--- a/src/Tricorder/CLI/Arguments.hs
+++ b/src/Tricorder/CLI/Arguments.hs
@@ -1,4 +1,4 @@
-module Tricorder.CLI.Arguments (runArguments, parseArguments) where
+module Tricorder.CLI.Arguments (reader) where
 
 import Atelier.Effects.Arguments (Arguments)
 import Effectful.Reader.Static (Reader)
@@ -13,14 +13,10 @@
 import Tricorder.Version qualified as Version
 
 
-runArguments :: (Arguments :> es) => Eff (Reader Command : es) a -> Eff es a
-runArguments eff = do
-    args <- parseArguments
+reader :: (Arguments :> es) => Eff (Reader Command : es) a -> Eff es a
+reader eff = do
+    args <- Args.execParser opts
     Reader.runReader args eff
-
-
-parseArguments :: (Arguments :> es) => Eff es Command
-parseArguments = Args.execParser opts
 
 
 opts :: ParserInfo Command
diff --git a/src/Tricorder/CLI/Daemon.hs b/src/Tricorder/CLI/Daemon.hs
--- a/src/Tricorder/CLI/Daemon.hs
+++ b/src/Tricorder/CLI/Daemon.hs
@@ -9,18 +9,19 @@
 import Atelier.Effects.Delay (Delay)
 import Atelier.Effects.File (File)
 import Atelier.Effects.Posix.Daemons (Daemons)
-import Atelier.Effects.Timeout (Timeout, timeout)
+import Atelier.Effects.Timeout (Timeout)
 import Atelier.Time (Millisecond, Second)
 import Effectful (IOE)
 import Effectful.NonDet (OnEmptyPolicy (..), emptyEff, runNonDet)
 import Effectful.Reader.Static (Reader, ask)
 import Effectful.Writer.Static.Local (runWriter, tell)
-import Tricorder.API.Force (Force)
+import Tricorder.API.IfThereArePendingQueries (IfThereArePendingQueries)
 import Prelude hiding (force)
 
 import Atelier.Effects.Delay qualified as Delay
 import Atelier.Effects.Posix.Daemons qualified as Daemons
-import Tricorder.API.Force qualified as Force
+import Atelier.Effects.Timeout qualified as Timeout
+import Tricorder.API.IfThereArePendingQueries qualified as IfThereArePendingQueries
 
 import Tricorder.Runtime (PidFile, SocketPath (..))
 import Tricorder.Socket.Client (isDaemonReady, isDaemonRunning, requestShutdown)
@@ -52,7 +53,7 @@
        , Timeout :> es
        , UnixSocket :> es
        )
-    => Force -> Eff es (Either [Text] Text)
+    => IfThereArePendingQueries -> Eff es (Either [Text] Text)
 stopDaemon force = do
     SocketPath sockPath <- ask
     pidFile <- ask
@@ -67,11 +68,11 @@
         (Nothing, es) -> pure $ Left es
   where
     timeoutDelay :: Second = case force of
-        Force.Force -> 3
-        Force.NoForce -> 6
+        IfThereArePendingQueries.DontWait -> 3
+        IfThereArePendingQueries.Wait -> 6
     requestStop sockPath pidFile = do
         timeout1second (requestShutdown force sockPath) >>= \_ -> do
-            didStop <- fmap isJust $ timeout timeoutDelay $ waitForStop pidFile
+            didStop <- fmap isJust $ Timeout.with timeoutDelay $ waitForStop pidFile
             if didStop
                 then
                     pure "Daemon stopped."
@@ -86,7 +87,7 @@
                 tell ["Daemon did not respond to SIGKILL: " <> show ex]
                 emptyEff
 
-    timeout1second = fmap (join . fmap rightToMaybe) . timeout (1 :: Second)
+    timeout1second = fmap (join . fmap rightToMaybe) . Timeout.with (1 :: Second)
 
     waitForStop :: forall es'. (Daemons :> es', Delay :> es') => PidFile -> Eff es' ()
     waitForStop pidFile = fix \rec -> do
@@ -113,7 +114,7 @@
        , Timeout :> es
        , UnixSocket :> es
        )
-    => Force
+    => IfThereArePendingQueries
     -> Eff es (Maybe (Either [Text] Text))
 restartDaemon force = do
     running <- isDaemonRunning
diff --git a/src/Tricorder/CLI/Main.hs b/src/Tricorder/CLI/Main.hs
--- a/src/Tricorder/CLI/Main.hs
+++ b/src/Tricorder/CLI/Main.hs
@@ -1,38 +1,36 @@
 module Tricorder.CLI.Main (main) where
 
-import Atelier.Effects.Arguments (runArgumentsIO)
-import Atelier.Effects.Clock (runClock)
-import Atelier.Effects.Conc (runConc)
-import Atelier.Effects.Console (Console, runConsole, runConsoleOut)
-import Atelier.Effects.Delay (runDelay)
-import Atelier.Effects.Env (runEnv)
-import Atelier.Effects.Exit (exitFailure, runExit)
-import Atelier.Effects.File (runFile)
-import Atelier.Effects.FileSystem (runFileSystemIO)
-import Atelier.Effects.Log (runLogNoOp)
-import Atelier.Effects.Posix.Daemons (runDaemons)
-import Atelier.Effects.Process (runProcessIO)
-import Atelier.Effects.Timeout (runTimeout)
+import Atelier.Effects.Console (Console)
 import Atelier.Signal (installTerminationHandler)
 import Data.Default (def)
 import Effectful (runEff)
 import Effectful.Concurrent (runConcurrent)
 import Effectful.Provider (provideWith_, runProvider_)
 
+import Atelier.Effects.Arguments qualified as Arguments
+import Atelier.Effects.Clock qualified as Clock
+import Atelier.Effects.Conc qualified as Conc
 import Atelier.Effects.Console qualified as Console
+import Atelier.Effects.Delay qualified as Delay
+import Atelier.Effects.Env qualified as Env
+import Atelier.Effects.Exit qualified as Exit
+import Atelier.Effects.File qualified as File
+import Atelier.Effects.FileSystem qualified as FileSystem
 import Atelier.Effects.FileSystem.Glob qualified as Glob
 import Atelier.Effects.Input qualified as Input
+import Atelier.Effects.Log qualified as Log
+import Atelier.Effects.Posix.Daemons qualified as Daemons
+import Atelier.Effects.Process qualified as Process
+import Atelier.Effects.Timeout qualified as Timeout
 
-import Tricorder.CLI.Arguments (runArguments)
 import Tricorder.CLI.UI.Brick (runBrick)
 import Tricorder.CLI.UI.BrickChan (runBrickChan)
 import Tricorder.Config (asInput)
 import Tricorder.Runtime (runLogPath, runPidFile, runProjectRoot, runRuntimeDir, runSocketPath)
 import Tricorder.Session.CabalFile (inputCabalFiles)
-import Tricorder.Session.CommandTemplate (CommandTemplate (..))
-import Tricorder.Session.Stage.Build.Session (Session (..))
 
 import Tricorder.CLI.App qualified as App
+import Tricorder.CLI.Arguments qualified as CLI.Arguments
 import Tricorder.CLI.UI.Keys qualified as Keys
 import Tricorder.Config.LoadedConfig qualified as LoadedConfig
 import Tricorder.Daemon.DaemonInfo qualified as DaemonInfo
@@ -47,18 +45,18 @@
 main :: IO ()
 main =
     runEff
-        . runTimeout
+        . Timeout.run
         . runConcurrent
-        . runConc
+        . Conc.runIO
         . runBrickChan
         . runBrick
-        . runConsole
-        . runProvider_ @Console @Console.Output runConsoleOut
-        . runExit
-        . runClock
-        . runDelay
-        . runFile
-        . runFileSystemIO
+        . Console.runIO
+        . runProvider_ @Console @Console.Output Console.runOut
+        . Exit.runIO
+        . Clock.runIO
+        . Delay.runIO
+        . File.run
+        . FileSystem.runIO
         . Glob.runIO
         . runProjectRoot
         . runRuntimeDir
@@ -69,13 +67,13 @@
         . LoadedConfig.asNestedInput @"keybindings" @Keys.Config
         . Input.map (either @_ @Keys.Config (const def) id)
         . Input.toReader @Keys.Config
-        . runDaemons
-        . runProcessIO
-        . runArgumentsIO
-        . runArguments
+        . Daemons.run
+        . Process.run
+        . Arguments.runIO
+        . CLI.Arguments.reader
         . UnixSocket.runIO
-        . runEnv
-        . runLogNoOp
+        . Env.runIO
+        . Log.runNoOp
         . StackProject.inputWithCache
         . inputCabalFiles
         . Tricorder.Config.asInput
@@ -85,13 +83,10 @@
                     provideWith_ @Console Console.Stderr
                         $ Console.putTextLn
                         $ "Failed to load session: " <> err
-                    exitFailure
+                    Exit.failure
                 Right session ->
                     Input.withConst session
-                        -- TODO: Make whatever uses this 'Repl' value use it
-                        -- more sensibly. Arbitrarily using the build stage's
-                        -- 'Repl' doesn't seem right.
-                        . Input.withConst session.buildSession.commandTemplate.repl
+                        . Input.withEff (Session.buildSession <$> Input.input)
                         . GhcPkg.runGhcPkgIO
                         . PackageStore.run
                         . Hackage.run
diff --git a/src/Tricorder/CLI/Operations.hs b/src/Tricorder/CLI/Operations.hs
--- a/src/Tricorder/CLI/Operations.hs
+++ b/src/Tricorder/CLI/Operations.hs
@@ -2,7 +2,6 @@
     ( showLog
     , showSource
     , showStatus
-    , showTests
     , showEvalComments
     , showDaemonInfo
     )
@@ -10,7 +9,7 @@
 
 import Atelier.Effects.Clock (Clock, currentTimeZone)
 import Atelier.Effects.Console (Console)
-import Atelier.Effects.Exit (Exit, exitFailure)
+import Atelier.Effects.Exit (Exit)
 import Atelier.Effects.File (File)
 import Atelier.Effects.FileSystem (FileSystem, doesFileExist, readFileLbs)
 import Atelier.Effects.Input (Input, input)
@@ -24,30 +23,26 @@
     ( EvalCommentsOptions (..)
     , SourceOptions (..)
     , StatusOptions (..)
-    , TestOptions (..)
     , Verbosity (..)
     )
+import Tricorder.API.IfBuildIsPending (IfBuildIsPending (..))
 import Tricorder.API.OutputFormat (OutputFormat (..))
-import Tricorder.API.WaitMode (WaitMode (..))
 import Tricorder.SourceLookup.ReexportLimits (ReexportLimits (..), defaultReexportLimits)
 
 import Atelier.Effects.Console qualified as Console
+import Atelier.Effects.Exit qualified as Exit
 import Data.ByteString.Lazy qualified as BSL
 import Data.Map.Strict qualified as Map
 import Data.Text qualified as T
+import Tricorder.API.IfBuildIsPending qualified as IfBuildIsPending
 
 import Tricorder.Build (BuildState (..), Severity (..))
 import Tricorder.Build.Duration (Duration (..))
 import Tricorder.Build.Test (Suites (..))
-import Tricorder.CLI.Render
-    ( diagnosticLineIndexed
-    , formatDuration
-    , renderSourceResults
-    )
+import Tricorder.CLI.Render (diagnosticLineIndexed, formatDuration, renderSourceResults)
 import Tricorder.Daemon.DaemonInfo (DaemonInfo (..))
 import Tricorder.Runtime (SocketPath (..))
 import Tricorder.Session.CabalFile (CabalFile)
-import Tricorder.Session.Repl (Repl)
 import Tricorder.Socket.Client (queryStatus, queryStatusWait)
 import Tricorder.Socket.UnixSocket (UnixSocket)
 import Tricorder.SourceLookup (lookupModuleSource, projectSourceDirs)
@@ -60,6 +55,7 @@
 import Tricorder.Build.EvalComment qualified as Eval
 import Tricorder.Build.Test qualified as Test
 import Tricorder.Build.Test qualified as Tests
+import Tricorder.Session.Stage.Build.Session qualified as Build (Session)
 import Tricorder.Session.Target qualified as Target
 import Tricorder.Session.TestTarget qualified as TestTarget
 
@@ -69,7 +65,7 @@
 reportBuildFailed msg = do
     Console.putTextLn "Build command failed:"
     Console.putTextLn msg
-    exitFailure
+    Exit.failure
 
 
 showStatus
@@ -139,7 +135,7 @@
                     mapM_ printDiag (zip [1 ..] result.diagnostics)
                     Console.putTextLn $ buildSummary tz result
                     mapM_ (uncurry (printTestRun verbosity)) $ Map.toList postBuild.testSuites.getSuites
-                    when (buildHasErrors result || Test.hasFailedTests postBuild.testSuites) exitFailure
+                    when (buildHasErrors result || Test.hasFailedTests postBuild.testSuites) Exit.failure
 
     printTestRun verbosity tgt tr = do
         Console.putTextLn $ case tr of
@@ -200,78 +196,12 @@
         else readFileLbs path >>= Console.putStr . BSL.toStrict
 
 
-showTests
-    :: ( Console :> es
-       , Exit :> es
-       , File :> es
-       , Reader SocketPath :> es
-       , UnixSocket :> es
-       )
-    => TestOptions -> Eff es ()
-showTests opts = do
-    result <- awaitBuildStatus opts.wait
-    case result of
-        Left err -> Console.putTextLn $ "Error: " <> err
-        Right state ->
-            case state.phase of
-                Build.Starting -> Console.putStrLn "Daemon starting, no test results yet."
-                Build.Building _ _ -> Console.putStrLn "Build in progress, no test results yet."
-                Build.PostBuilding _ postBuild -> renderTestRuns postBuild.testSuites.getSuites
-                Build.Finished _ postBuild -> renderTestRuns postBuild.testSuites.getSuites
-                Build.Failed msg -> reportBuildFailed msg
-  where
-    renderTestRuns suites
-        | Map.null suites = Console.putStrLn "No test results."
-        | Map.null filteredSuites = do
-            Console.putStrLn "All passed."
-            mapM_ (Console.putTextLn . ("  " <>) . TestTarget.render) $ Map.keys suites
-        | otherwise = do
-            mapM_ (uncurry printTestOutput) $ Map.toList filteredSuites
-            when (any Test.isFailedRun filteredSuites) exitFailure
-      where
-        filteredSuites =
-            if opts.failedOnly
-                then
-                    Map.filter Test.isFailedRun suites
-                else
-                    suites
-
-    printTestOutput tgt tr = case tr of
-        Test.SuiteRunning Nothing ->
-            Console.putTextLn $ t <> "running..."
-        Test.SuiteRunning (Just p) ->
-            Console.putTextLn $ t <> "running... (" <> show p.compiled <> "/" <> show p.total <> ")"
-        Test.SuiteErrored e ->
-            Console.putTextLn $ t <> "error: " <> e.message
-        Test.SuiteCompleted c -> do
-            Console.putTextLn $ t <> completionSummary c
-            if opts.failedOnly
-                then
-                    if null c.testCases
-                        then do
-                            Console.putTextLn "  (unrecognised test runner format — showing full output)"
-                            mapM_ (Console.putTextLn . ("  " <>)) (stripGhciNoise (lines c.output))
-                        else
-                            mapM_ printFailedCase (filter Test.caseFailed c.testCases)
-                else
-                    mapM_ (Console.putTextLn . ("  " <>)) (stripGhciNoise (lines c.output))
-      where
-        t = TestTarget.render tgt <> "  "
-
-    printFailedCase tc = do
-        Console.putTextLn $ "  " <> tc.description
-        case tc.outcome of
-            Test.Failed details ->
-                mapM_ (Console.putTextLn . ("    " <>)) (T.lines details)
-            Test.Passed -> pure ()
-
-
 showSource
     :: ( Console :> es
        , FileSystem :> es
        , GhcPkg :> es
        , Hackage :> es
-       , Input Repl :> es
+       , Input Build.Session :> es
        , Input [CabalFile] :> es
        , Log :> es
        , PackageStore :> es
@@ -332,7 +262,7 @@
 displayTextOutput = \case
     Left err -> do
         Console.putTextLn $ "Error: " <> err
-        exitFailure
+        Exit.failure
     Right (BuildState phase _) -> case phase of
         Build.Starting -> Console.putStrLn "Starting..."
         Build.Building _ _ -> Console.putStrLn "Building..."
@@ -340,7 +270,7 @@
         Build.Finished _ postBuild -> txtEvalComments postBuild
         Build.Failed err -> do
             Console.putTextLn $ "Error: " <> err
-            exitFailure
+            Exit.failure
   where
     txtEvalComments postBuild =
         let evalutatingComments = Eval.phasePending postBuild.evalComments
@@ -387,13 +317,13 @@
 displayJsonOutput = \case
     Left err -> do
         putJson $ Eval.Failed err
-        exitFailure
+        Exit.failure
     Right (BuildState phase _) -> case phase of
         Build.Starting -> putJson Eval.Starting
         Build.Building _ _ -> putJson Eval.Building
         Build.Failed msg -> do
             putJson $ Eval.Failed msg
-            exitFailure
+            Exit.failure
         Build.PostBuilding _ postBuild -> jsonEvalComments postBuild
         Build.Finished _ postBuild -> jsonEvalComments postBuild
   where
@@ -444,9 +374,9 @@
        , Reader SocketPath :> es
        , UnixSocket :> es
        )
-    => WaitMode -> Eff es (Either Text BuildState)
+    => IfBuildIsPending -> Eff es (Either Text BuildState)
 awaitBuildStatus wait = do
     SocketPath sockPath <- ask
     case wait of
-        WaitForBuild -> queryStatusWait sockPath
-        ShowCurrent -> queryStatus sockPath
+        IfBuildIsPending.WaitForBuild -> queryStatusWait sockPath
+        IfBuildIsPending.UseCurrent -> queryStatus sockPath
diff --git a/src/Tricorder/CLI/UI/Keys.hs b/src/Tricorder/CLI/UI/Keys.hs
--- a/src/Tricorder/CLI/UI/Keys.hs
+++ b/src/Tricorder/CLI/UI/Keys.hs
@@ -9,7 +9,7 @@
 where
 
 import Atelier.Effects.Console (Console)
-import Atelier.Effects.Exit (Exit, exitFailure)
+import Atelier.Effects.Exit (Exit)
 import Brick
     ( EventM
     , Widget
@@ -49,6 +49,7 @@
 import Text.Casing (quietSnake)
 
 import Atelier.Effects.Console qualified as Console
+import Atelier.Effects.Exit qualified as Exit
 import Data.Map.Strict qualified as Map
 import Data.Set qualified as Set
 import Data.Text qualified as T
@@ -154,7 +155,7 @@
     unless (null errors) do
         Console.putTextLn "Error(s) encountered when attempting to parse key bindings:"
         traverse_ (Console.putTextLn . toText) errors
-        exitFailure
+        Exit.failure
     pure customBindings
 
 
@@ -228,7 +229,7 @@
                 $ T.intercalate "\n\n"
                 $ "Your key bindings have collisions:"
                     : (uncurry showCollision <$> collisions)
-            exitFailure
+            Exit.failure
         Right dispatcher -> pure dispatcher
   where
     showCollision binding' handlers =
diff --git a/src/Tricorder/Config.hs b/src/Tricorder/Config.hs
--- a/src/Tricorder/Config.hs
+++ b/src/Tricorder/Config.hs
@@ -6,7 +6,7 @@
 
 import Atelier.Effects.Input (Input)
 import Atelier.Types.JSON.QuietSnake (QuietSnake (..))
-import Atelier.Types.WithDefaults (WithDefaults (..))
+import Atelier.Types.JSON.WithDefaults (WithDefaults (..))
 import Data.Aeson (FromJSON, ToJSON)
 import Data.Default (Default (..))
 
diff --git a/src/Tricorder/Daemon/Builder.hs b/src/Tricorder/Daemon/Builder.hs
--- a/src/Tricorder/Daemon/Builder.hs
+++ b/src/Tricorder/Daemon/Builder.hs
@@ -11,7 +11,6 @@
 where
 
 import Atelier.Effects.Clock (Clock, UTCTime)
-import Atelier.Effects.FileWatcher (FileEvent)
 import Atelier.Effects.Log (Log)
 import Atelier.Effects.PubSub.Pub (Pub)
 import Atelier.Time (nominalDiffTime)
@@ -25,6 +24,7 @@
 import System.FilePath (normalise)
 
 import Atelier.Effects.Clock qualified as Clock
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher (Event)
 import Atelier.Effects.Log qualified as Log
 import Atelier.Effects.PubSub.Pub qualified as Pub
 import Data.Map.Strict qualified as Map
@@ -58,7 +58,7 @@
 
 
 data Builder :: Effect where
-    Consider :: FilePath -> FileEvent -> Builder m BuildConsideration
+    Consider :: FilePath -> FileWatcher.Event -> Builder m BuildConsideration
     Build :: DispatchAction -> Builder m (Either BuildFailure NewLoadResult)
 
 
@@ -127,7 +127,7 @@
 considerBuild
     :: (State BuilderState :> es)
     => FilePath
-    -> FileEvent
+    -> FileWatcher.Event
     -> Eff es BuildConsideration
 considerBuild fp event = do
     builderState <- get
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
@@ -3,7 +3,6 @@
 import Atelier.Effects.Chan (Chan)
 import Atelier.Effects.Clock (Clock)
 import Atelier.Effects.Conc (Conc)
-import Atelier.Effects.FileWatcher (FileEvent)
 import Atelier.Effects.Log (Log)
 import Atelier.Effects.Process (Process)
 import Atelier.Effects.PubSub.Pub (Pub)
@@ -15,6 +14,7 @@
 import Relude.Extra.Tuple (dup)
 
 import Atelier.Effects.Conc qualified as Conc
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher (Event)
 import Atelier.Effects.Log qualified as Log
 import Atelier.Effects.PubSub qualified as PubSub
 import Atelier.Effects.PubSub.Pub qualified as Pub
@@ -24,7 +24,6 @@
 import Effectful.State.Static.Shared qualified as State
 
 import Tricorder.Build (BuildId, BuildPhase, BuildResult, PostBuild (..), Severity (..))
-import Tricorder.Build.ByteSize (ByteSize)
 import Tricorder.Build.Changes (CabalChangeDetected (..), SourceChangeDetected (..))
 import Tricorder.Daemon.Builder
     ( BuildConsideration (..)
@@ -40,7 +39,6 @@
 import Tricorder.Runtime (ProjectRoot (..))
 import Tricorder.Session (Session (..))
 import Tricorder.Session.Stage.Test.Command (RenderedTestCommand (..))
-import Tricorder.Session.TestTimeout (TestTimeout (..))
 import Tricorder.Waiters (Waiters)
 
 import Tricorder.Build qualified as Build
@@ -66,7 +64,7 @@
 data RestartBuilder = RestartBuilder
 
 
-data ReloadBuilder = ReloadBuilder FilePath FileEvent
+data ReloadBuilder = ReloadBuilder FilePath FileWatcher.Event
 
 
 -- | Top of the build loop. Responsible for handling changes to the Tricorder
@@ -282,10 +280,7 @@
     => Session -> BuildResult -> Eff es Test.Suites
 runTests session buildResult
     | hasTargets session.testSession.targets && noErrors buildResult.diagnostics =
-        runTestsForTargets
-            session.testSession
-            session.testMemoryLimit
-            session.testTimeout
+        runTestsForTargets session.testSession
     | otherwise = pure mempty
   where
     hasTargets = not . null
@@ -298,10 +293,8 @@
        , TestRunner :> es
        )
     => Test.Session
-    -> Maybe ByteSize
-    -> TestTimeout
     -> Eff es Test.Suites
-runTestsForTargets testSession memoryLimit testTimeout = do
+runTestsForTargets testSession = do
     Pub.publish $ Test.Suites initial
     Log.info $ "Running " <> show (length testSession.targets) <> " test suite(s)"
     fmap Test.Suites . State.execState initial $ traverse_ go testSession.targets
@@ -309,7 +302,7 @@
     initial = Map.fromList $ (,Test.SuiteRunning Nothing) <$> testSession.targets
     go target = do
         Log.info $ "Running tests: " <> renderedTarget
-        let testCommand = Test.Command.render testSession memoryLimit target
+        let testCommand = Test.Command.render testSession target
             publishProgress suite = do
                 updated <- State.state $ dup . Map.insert target suite
                 Pub.publish $ Test.Suites updated
@@ -318,7 +311,7 @@
                 <> renderedTarget
                 <> " command:\n"
                 <> show testCommand.command
-        finishedSuite <- TestRunner.runTestSuite testSession publishProgress testTimeout testCommand
+        finishedSuite <- TestRunner.runTestSuite testSession publishProgress testCommand
         case finishedSuite of
             Test.SuiteErrored (Test.SuiteError message) ->
                 Log.warn $ "Test suite " <> renderedTarget <> "failed: " <> message
diff --git a/src/Tricorder/Daemon/Dispatch.hs b/src/Tricorder/Daemon/Dispatch.hs
--- a/src/Tricorder/Daemon/Dispatch.hs
+++ b/src/Tricorder/Daemon/Dispatch.hs
@@ -12,9 +12,10 @@
     )
 where
 
-import Atelier.Effects.FileWatcher (FileEvent (..))
 import System.FilePath (isAbsolute, normalise, splitDirectories, takeExtension, (</>))
 
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher (Event)
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher.Event
 import Data.List qualified as List
 import Data.Map.Strict qualified as Map
 import Data.Set qualified as Set
@@ -126,22 +127,22 @@
     :: KnownTargetNames
     -> Maybe Ghci.LoadedModule
     -> FilePath
-    -> FileEvent
+    -> FileWatcher.Event
     -> Maybe DispatchAction
 dispatch knownTargets known fp event = case known of
     Just lm -> Just $ case event of
-        Added -> Reload
-        Modified -> Reload
-        Removed -> Unadd lm.moduleName
+        FileWatcher.Event.Added -> Reload
+        FileWatcher.Event.Modified -> Reload
+        FileWatcher.Event.Removed -> Unadd lm.moduleName
     Nothing
         | fileMatchesAnyTarget knownTargets fp -> case event of
-            Added -> Just Reload
-            Modified -> Just Reload
-            Removed -> Nothing
+            FileWatcher.Event.Added -> Just Reload
+            FileWatcher.Event.Modified -> Just Reload
+            FileWatcher.Event.Removed -> Nothing
         | otherwise -> case event of
-            Added -> Just (Add fp)
-            Modified -> Just (Add fp)
-            Removed -> Nothing
+            FileWatcher.Event.Added -> Just (Add fp)
+            FileWatcher.Event.Modified -> Just (Add fp)
+            FileWatcher.Event.Removed -> Nothing
 
 
 -- | Keep only diagnostics whose file is under one of the watched directories.
diff --git a/src/Tricorder/Daemon/Ghci/Config/File.hs b/src/Tricorder/Daemon/Ghci/Config/File.hs
--- a/src/Tricorder/Daemon/Ghci/Config/File.hs
+++ b/src/Tricorder/Daemon/Ghci/Config/File.hs
@@ -6,17 +6,13 @@
 
 import Atelier.Time (AsRawUnit (..), Millisecond)
 import Atelier.Types.JSON.QuietSnake (QuietSnake (..))
-import Atelier.Types.WithDefaults
-    ( WithDefaults (..)
-    )
+import Atelier.Types.JSON.WithDefaults (WithDefaults (..))
 import Data.Aeson (FromJSON, ToJSON)
 import Data.Default (Default (..))
 
 import Atelier.Time qualified as Time
 
-import Tricorder.Daemon.Ghci.Config
-    ( Config (..)
-    )
+import Tricorder.Daemon.Ghci.Config (Config (..))
 
 
 data File = File
diff --git a/src/Tricorder/Daemon/Ghci/Process.hs b/src/Tricorder/Daemon/Ghci/Process.hs
--- a/src/Tricorder/Daemon/Ghci/Process.hs
+++ b/src/Tricorder/Daemon/Ghci/Process.hs
@@ -30,7 +30,7 @@
     , setWorkingDir
     , shell
     )
-import Atelier.Effects.Timeout (Timeout, timeout)
+import Atelier.Effects.Timeout (Timeout)
 import Control.Concurrent.STM (TVar, modifyTVar', readTVar, retry, writeTVar)
 import Data.Sequence ((|>))
 import Data.Time.Units (Second)
@@ -43,6 +43,7 @@
 import Atelier.Effects.Log qualified as Log
 import Atelier.Effects.Process qualified as Atelier
 import Atelier.Effects.Process qualified as Process
+import Atelier.Effects.Timeout qualified as Timeout
 import Data.Text qualified as T
 
 import Tricorder.Daemon.Ghci.Config (Config (..))
@@ -289,7 +290,7 @@
     void $ trySync $ do
         File.hPutTextLn ghciProcess.stdin ":quit"
         File.hFlush ghciProcess.stdin
-    void $ timeout config.shutdownTimeout (Process.waitExitCode ghciProcess.handle)
+    void $ Timeout.with config.shutdownTimeout (Process.waitExitCode ghciProcess.handle)
 
 
 -- ---------------------------------------------------------------------------
@@ -407,7 +408,7 @@
 
     Conc.scoped do
         stderrThread <- Conc.fork drainStderr
-        result <- timeout delay $ trySync watchStdout
+        result <- Timeout.with delay $ trySync watchStdout
         case result of
             Just (Right ()) -> pure ()
             Just (Left ex) -> do
@@ -416,7 +417,7 @@
                 -- pipe lingers) so we surface its *complete* output — e.g.
                 -- cabal's dependency-resolution error — rather than whatever
                 -- the drain happened to have read so far.
-                _ <- timeout (1 :: Second) (Conc.await stderrThread)
+                _ <- Timeout.with (1 :: Second) (Conc.await stderrThread)
                 captured <- atomically $ readTVar capturedVar
                 throwIO $ StartupFailed $ fromMaybe (startupExitedMessage ex) (renderCapturedLines captured)
             Nothing -> do
diff --git a/src/Tricorder/Daemon/HookDispatch.hs b/src/Tricorder/Daemon/HookDispatch.hs
--- a/src/Tricorder/Daemon/HookDispatch.hs
+++ b/src/Tricorder/Daemon/HookDispatch.hs
@@ -1,10 +1,11 @@
 module Tricorder.Daemon.HookDispatch (main) where
 
-import Atelier.Effects.FileWatcher (FileEvent (..))
 import Atelier.Effects.Process (Process)
 import Atelier.Effects.PubSub.Sub (Sub)
 import Effectful.Reader.Static (Reader)
 
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher.Event
 import Atelier.Effects.PubSub.Sub qualified as Sub
 import System.FilePath qualified as Path
 
@@ -27,8 +28,9 @@
             | otherwise -> pure ()
 
 
-runHsHook :: (Process :> es, Reader ProjectRoot :> es) => HaskellHooks -> FileEvent -> Eff es ()
+runHsHook
+    :: (Process :> es, Reader ProjectRoot :> es) => HaskellHooks -> FileWatcher.Event -> Eff es ()
 runHsHook hooks = \case
-    Added -> whenJust hooks.fileAdded Hook.run
-    Modified -> whenJust hooks.fileModified Hook.run
-    Removed -> whenJust hooks.fileRemoved Hook.run
+    FileWatcher.Event.Added -> whenJust hooks.fileAdded Hook.run
+    FileWatcher.Event.Modified -> whenJust hooks.fileModified Hook.run
+    FileWatcher.Event.Removed -> whenJust hooks.fileRemoved Hook.run
diff --git a/src/Tricorder/Daemon/Hpack.hs b/src/Tricorder/Daemon/Hpack.hs
--- a/src/Tricorder/Daemon/Hpack.hs
+++ b/src/Tricorder/Daemon/Hpack.hs
@@ -11,6 +11,7 @@
 
 import Atelier.Effects.Conc qualified as Conc
 import Atelier.Effects.FileWatcher qualified as FileWatcher
+import Atelier.Effects.FileWatcher.Watch qualified as FileWatcher.Watch
 import Atelier.Effects.Log qualified as Log
 import Atelier.Effects.PubSub qualified as PubSub
 import Atelier.Effects.PubSub.Pub qualified as Pub
@@ -73,7 +74,7 @@
     => ProjectRoot -> Eff es Void
 watchPackageYaml root = do
     FileWatcher.watchFilePathsDebounced
-        [FileWatcher.dirWhere root.getProjectRoot (packageYaml `List.isSuffixOf`)]
+        [FileWatcher.Watch.dirWhere root.getProjectRoot (packageYaml `List.isSuffixOf`)]
         \path _ -> Pub.publish $ RePack path
 
 
diff --git a/src/Tricorder/Daemon/IdleTimer.hs b/src/Tricorder/Daemon/IdleTimer.hs
--- a/src/Tricorder/Daemon/IdleTimer.hs
+++ b/src/Tricorder/Daemon/IdleTimer.hs
@@ -8,7 +8,7 @@
 import Atelier.Effects.Clock (Clock, currentTime)
 import Atelier.Effects.Conc (Conc)
 import Atelier.Effects.Delay (Delay)
-import Atelier.Effects.Exit (Exit, exitSuccess)
+import Atelier.Effects.Exit (Exit)
 import Atelier.Effects.Input (Input, input)
 import Atelier.Effects.Log (Log)
 import Atelier.Time (Second, nominalDiffTime)
@@ -22,6 +22,7 @@
 
 import Atelier.Effects.Conc qualified as Conc
 import Atelier.Effects.Delay qualified as Delay
+import Atelier.Effects.Exit qualified as Exit
 import Atelier.Effects.Log qualified as Log
 
 import Tricorder.Session.IdleTimeout (IdleTimeout (..))
@@ -78,7 +79,7 @@
                             $ "Idle for "
                                 <> show secs
                                 <> " with no active connections, shutting down."
-                        exitSuccess
+                        Exit.success
 
     interpretWith act \env -> \case
         WithActivity action -> do
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
@@ -1,29 +1,29 @@
 module Tricorder.Daemon.Main (main) where
 
-import Atelier.Effects.Chan (runChan)
-import Atelier.Effects.Clock (runClock)
-import Atelier.Effects.Conc (runConc)
-import Atelier.Effects.Debounce (runDebounce)
-import Atelier.Effects.Delay (runDelay)
-import Atelier.Effects.Env (runEnv)
-import Atelier.Effects.Exit (runExit)
-import Atelier.Effects.File (runFile)
-import Atelier.Effects.FileSystem (runFileSystemIO)
-import Atelier.Effects.FileWatcher (runFileWatcherIO)
-import Atelier.Effects.Process (runProcessIO)
-import Atelier.Effects.Timeout (runTimeout)
 import Atelier.Signal (installTerminationHandler)
 import Data.Default (def)
 import Effectful (runEff)
 import Effectful.Concurrent (runConcurrent)
 
+import Atelier.Effects.Chan qualified as Chan
+import Atelier.Effects.Clock qualified as Clock
 import Atelier.Effects.Conc qualified as Conc
+import Atelier.Effects.Debounce qualified as Debounce
+import Atelier.Effects.Delay qualified as Delay
+import Atelier.Effects.Env qualified as Env
+import Atelier.Effects.Exit qualified as Exit
+import Atelier.Effects.FSNotify qualified as FSNotify
+import Atelier.Effects.File qualified as File
+import Atelier.Effects.FileSystem qualified as FileSystem
 import Atelier.Effects.FileSystem.Glob qualified as Glob
+import Atelier.Effects.FileWatcher qualified as FileWatcher
 import Atelier.Effects.Input qualified as Input
 import Atelier.Effects.Log qualified as Log
+import Atelier.Effects.Process qualified as Process
 import Atelier.Effects.PubSub qualified as PubSub
 import Atelier.Effects.PubSub.Pub qualified as Pub
 import Atelier.Effects.PubSub.Sub qualified as Sub
+import Atelier.Effects.Timeout qualified as Timeout
 import Effectful.Reader.Static qualified as Reader
 import Effectful.State.Static.Shared qualified as State
 
@@ -62,27 +62,28 @@
 main =
     runEff
         . runConcurrent
-        . runConc
-        . runClock
-        . runDelay
-        . runTimeout
-        . runDebounce @FilePath
-        . runFileWatcherIO
-        . runFileSystemIO
+        . Conc.runIO
+        . Clock.runIO
+        . Delay.runIO
+        . Timeout.run
+        . Debounce.run @FilePath
+        . FileSystem.runIO
+        . FSNotify.run
+        . FileWatcher.runIO
         . Glob.runIO
         . runProjectRoot
-        . runExit
-        . runFile
+        . Exit.runIO
+        . File.run
         . runRuntimeDir
         . runSocketPath
         . runLogPath
-        . runEnv
+        . Env.runIO
         . LoadedConfig.asInput
         . runLogging
-        . runChan
+        . Chan.run
         . StackProject.inputWithCache
         . inputCabalFiles
-        . runProcessIO
+        . Process.run
         . runGhcPkgIO
         . UnixSocket.runIO
         . Ghci.Session.run
diff --git a/src/Tricorder/Daemon/TestRunner.hs b/src/Tricorder/Daemon/TestRunner.hs
--- a/src/Tricorder/Daemon/TestRunner.hs
+++ b/src/Tricorder/Daemon/TestRunner.hs
@@ -31,7 +31,7 @@
     , setWorkingDir
     , shell
     )
-import Atelier.Effects.Timeout (Timeout, timeout)
+import Atelier.Effects.Timeout (Timeout)
 import Control.Concurrent.STM (modifyTVar')
 import Control.Exception (throwIO)
 import Data.Sequence ((|>))
@@ -49,6 +49,8 @@
 import Atelier.Effects.Conc qualified as Conc
 import Atelier.Effects.File qualified as File
 import Atelier.Effects.Process qualified as Process
+import Atelier.Effects.Timeout qualified as Timeout
+import Atelier.Time qualified as Time
 import Data.List qualified as List
 import Data.Text qualified as T
 
@@ -59,7 +61,6 @@
 import Tricorder.Session.Stage.Test.Command (RenderedTestCommand (..))
 import Tricorder.Session.Stage.Test.Config (OutputMode (..))
 import Tricorder.Session.Stage.Test.Session (ResolvedTestOptions (..), Session (..))
-import Tricorder.Session.TestTimeout (TestTimeout (..))
 import Tricorder.TestOutput (parseHspecDuration, parseHspecOutput)
 
 import Tricorder.Build.Test qualified as Test
@@ -75,7 +76,6 @@
         :: Session
         -> (Test.Suite -> m ())
         -- ^ Handler for test run progress
-        -> TestTimeout
         -> RenderedTestCommand
         -> TestRunner m Test.Suite
 
@@ -98,12 +98,12 @@
     => Eff (TestRunner : es) a -> Eff es a
 run act = do
     interpretWith act \env -> \case
-        RunTestSuite session progressHandler testTimeout cmd ->
+        RunTestSuite session progressHandler cmd ->
             localUnlift env (ConcUnlift Persistent Unlimited) \unlift -> do
                 let onProgress = unlift . progressHandler . loadingToProgress
                 case cmd.options.outputMode of
-                    ReplOutput -> runTestSuiteWithGHCi session onProgress testTimeout cmd.command
-                    StdoutOutput -> runTestSuiteWithStdout onProgress testTimeout cmd.command
+                    ReplOutput -> runTestSuiteWithGHCi session onProgress cmd.command
+                    StdoutOutput -> runTestSuiteWithStdout session onProgress cmd.command
 
 
 runTestSuiteWithStdout
@@ -114,11 +114,11 @@
        , Reader ProjectRoot :> es
        , Timeout :> es
        )
-    => (Ghci.Loading -> Eff es ())
-    -> TestTimeout
+    => Session
+    -> (Ghci.Loading -> Eff es ())
     -> RenderedCommand 'Test
     -> Eff es Test.Suite
-runTestSuiteWithStdout onProgress testTimeout cmd = do
+runTestSuiteWithStdout session onProgress cmd = do
     ProjectRoot projectRoot <- ask
     -- Lines from both streams, in the order they arrived.
     outputVar <- newTVarIO (mempty :: Seq Text)
@@ -149,11 +149,9 @@
 
     result <- trySync
         $ Process.withProcessGroup processConfig \p ->
-            case testTimeout of
-                TestTimeout secs | secs <= 0 -> Right <$> runToCompletion p
-                TestTimeout secs ->
-                    let duration = fromIntegral secs :: Second
-                    in  maybeToRight secs <$> timeout duration (runToCompletion p)
+            case Time.convertUnit @_ @Second session.options.timeout of
+                secs | secs <= 0 -> Right <$> runToCompletion p
+                secs -> maybeToRight secs <$> Timeout.with secs (runToCompletion p)
     output <- T.unlines . toList <$> readTVarIO outputVar
     let completed passed =
             Test.SuiteCompleted
@@ -191,19 +189,17 @@
        )
     => Session
     -> (Ghci.Loading -> Eff es ())
-    -> TestTimeout
     -> RenderedCommand 'Test
     -> Eff es Test.Suite
-runTestSuiteWithGHCi session onProgress testTimeout cmd = do
+runTestSuiteWithGHCi session onProgress cmd = do
     ProjectRoot projectRoot <- ask
     result <- trySync
         $ Ghci.Process.with session.ghci cmd projectRoot onProgress noReady \ghci _ ->
-            case testTimeout of
-                TestTimeout secs | secs <= 0 -> Right <$> Ghci.Process.exec ghci ":main" noProgress
-                TestTimeout secs ->
-                    let duration = fromIntegral secs :: Second
-                    in  maybeToRight secs
-                            <$> timeout duration (Ghci.Process.exec ghci ":main" noProgress)
+            case Time.convertUnit @_ @Second session.options.timeout of
+                secs | secs <= 0 -> Right <$> Ghci.Process.exec ghci ":main" noProgress
+                secs ->
+                    maybeToRight secs
+                        <$> Timeout.with secs (Ghci.Process.exec ghci ":main" noProgress)
     case result of
         Left ex -> case fromException ex of
             Just (e :: Ghci.Process.Error) -> case e of
@@ -261,7 +257,7 @@
 runScripted results =
     reinterpret_
         (evalState results)
-        (\(RunTestSuite _ _ _ _) -> popResult)
+        (\(RunTestSuite _ _ _) -> popResult)
   where
     popResult :: Eff (State [Either SomeException Test.Suite] : es) Test.Suite
     popResult =
diff --git a/src/Tricorder/Daemon/Watch.hs b/src/Tricorder/Daemon/Watch.hs
--- a/src/Tricorder/Daemon/Watch.hs
+++ b/src/Tricorder/Daemon/Watch.hs
@@ -8,21 +8,16 @@
 where
 
 import Atelier.Effects.Debounce (Debounce)
-import Atelier.Effects.FileWatcher
-    ( FileEvent
-    , FileWatcher
-    , Watch
-    , containing
-    , dirExt
-    , dirWhere
-    , excluding
-    )
+import Atelier.Effects.FileWatcher (FileWatcher)
 import Atelier.Effects.PubSub.Pub (Pub)
 import System.FilePath (takeExtension, takeFileName)
 import Text.Regex.TDFA (ExecOption (..), blankCompOpt, blankExecOpt, match)
 import Text.Regex.TDFA.TDFA (patternToRegex)
 
 import Atelier.Effects.FileWatcher qualified as FileWatcher
+import Atelier.Effects.FileWatcher.Event qualified as FileWatcher (Event)
+import Atelier.Effects.FileWatcher.Watch qualified as FileWatcher (Watch)
+import Atelier.Effects.FileWatcher.Watch qualified as Watch
 import Atelier.Effects.PubSub.Pub qualified as Pub
 
 import Tricorder.Build.Changes
@@ -38,7 +33,7 @@
 
 data WatchedFile = WatchedFile
     { path :: FilePath
-    , event :: FileEvent
+    , event :: FileWatcher.Event
     }
 
 
@@ -72,18 +67,18 @@
     | otherwise = SourceChange
 
 
-specs :: ProjectRoot -> WatchExclusionPatterns -> WatchDirs -> [Watch]
+specs :: ProjectRoot -> WatchExclusionPatterns -> WatchDirs -> [FileWatcher.Watch]
 specs projectRoot watchExclusionPatterns watchDirs =
     sourceWatches (coerce watchExclusionPatterns) (coerce watchDirs)
         <> cabalWatches projectRoot
 
 
-sourceWatches :: [Pattern] -> [FilePath] -> [Watch]
+sourceWatches :: [Pattern] -> [FilePath] -> [FileWatcher.Watch]
 sourceWatches exclusionPatterns =
     map \d ->
-        dirExt d ".hs"
-            `excluding` containing "dist-newstyle"
-            `excluding` exclusionMatches exclusionPatterns
+        Watch.dirExt d ".hs"
+            `Watch.excluding` Watch.containing "dist-newstyle"
+            `Watch.excluding` exclusionMatches exclusionPatterns
 
 
 exclusionMatches :: [Pattern] -> FilePath -> Bool
@@ -95,9 +90,11 @@
             fp
 
 
-cabalWatches :: ProjectRoot -> [Watch]
+cabalWatches :: ProjectRoot -> [FileWatcher.Watch]
 cabalWatches (ProjectRoot projectRoot) =
-    [dirWhere projectRoot isCabalFile `excluding` containing "dist-newstyle"]
+    [ Watch.dirWhere projectRoot isCabalFile
+        `Watch.excluding` Watch.containing "dist-newstyle"
+    ]
 
 
 isCabalFile :: FilePath -> Bool
diff --git a/src/Tricorder/Logging.hs b/src/Tricorder/Logging.hs
--- a/src/Tricorder/Logging.hs
+++ b/src/Tricorder/Logging.hs
@@ -3,11 +3,7 @@
 import Atelier.Effects.Env (Env)
 import Atelier.Effects.File (BufferMode (..), File)
 import Atelier.Effects.Input (Input)
-import Atelier.Effects.Log
-    ( Log
-    , Severity (..)
-    , runLogToHandle
-    )
+import Atelier.Effects.Log (Log, Severity (..))
 import Effectful (IOE)
 import Effectful.Reader.Static (Reader, asks)
 
@@ -31,10 +27,10 @@
     => Eff (Log : es) a -> Eff es a
 runLogging act = do
     path <- asks @LogPath (.getLogPath)
-    File.withFile path AppendMode \h -> do
+    File.with path AppendMode \h -> do
         File.hSetBuffering h LineBuffering
         (maybeErr, sev) <- getMinimumSeverity
-        runLogToHandle h sev do
+        Log.runToHandle h sev do
             whenJust maybeErr \err -> Log.err err
             act
 
diff --git a/src/Tricorder/Session.hs b/src/Tricorder/Session.hs
--- a/src/Tricorder/Session.hs
+++ b/src/Tricorder/Session.hs
@@ -17,7 +17,6 @@
 import Data.Text qualified as T
 import Effectful.Reader.Static qualified as Reader
 
-import Tricorder.Build.ByteSize (ByteSize)
 import Tricorder.Runtime (ProjectRoot (..))
 import Tricorder.Session.CabalFile (CabalFile)
 import Tricorder.Session.CommandConfig (CommandConfig (..))
@@ -31,7 +30,6 @@
 import Tricorder.Session.Stage.Test.Config (TestConfig (..))
 import Tricorder.Session.Target (definesCustomPrelude)
 import Tricorder.Session.TestTarget (getTestTarget)
-import Tricorder.Session.TestTimeout (TestTimeout (..))
 import Tricorder.Session.Util (displayList, indent)
 import Tricorder.Session.WatchDirs (WatchDirs (..))
 import Tricorder.Session.WatchExclusionPatterns
@@ -39,7 +37,6 @@
     , resolveWatchExclusionPatterns
     )
 
-import Tricorder.Build.ByteSize qualified as ByteSize
 import Tricorder.Config qualified as Tricorder
 import Tricorder.Session.Hooks qualified as Hooks
 import Tricorder.Session.Stage.Build.Session qualified as Build (Session)
@@ -55,11 +52,9 @@
     { buildSession :: Build.Session
     , testSession :: Test.Session
     , evalSession :: Eval.Session
-    , testMemoryLimit :: Maybe ByteSize
     , watchDirs :: WatchDirs
     , watchExclusionPatterns :: WatchExclusionPatterns
     , replBuildDir :: ReplBuildDir
-    , testTimeout :: TestTimeout
     , generateWithHpack :: GenerateWithHpack
     , hooks :: Hooks
     , idleTimeout :: IdleTimeout
@@ -73,11 +68,9 @@
             { buildSession = def
             , testSession = def
             , evalSession = def
-            , testMemoryLimit = Nothing
             , watchDirs = def
             , watchExclusionPatterns = def
             , replBuildDir = def
-            , testTimeout = def
             , generateWithHpack = def
             , hooks = def
             , idleTimeout = def
@@ -111,15 +104,6 @@
     warnDeprecatedConfig cfgFile
     Log.debug "Warned about deprecated config stuff"
 
-    testMemoryLimit <- case cfgFile.testMemoryLimit of
-        Nothing -> pure Nothing
-        Just limit -> case ByteSize.fromText limit of
-            Nothing -> do
-                Log.err $ "Unable to parse test_memory_limit: " <> limit
-                pure Nothing
-            Just parsedLimit ->
-                pure $ Just parsedLimit
-
     watchExclusionPatterns <-
         case resolveWatchExclusionPatterns cfgFile.watchExclusionPatterns of
             Left err -> do
@@ -135,8 +119,8 @@
 
     repl <- resolveRepl projectRoot
     buildSession <- Build.Session.resolve cfgFile repl projectFiles
-    let testSession = Test.Session.resolve repl buildSession.targets cfgFile
-        evalSession = Eval.Session.resolve repl cfgFile
+    testSession <- Test.Session.resolve repl buildSession.targets cfgFile
+    let evalSession = Eval.Session.resolve repl cfgFile
         effectiveTargets = buildSession.targets <> (getTestTarget <$> testSession.targets)
         watchDirs = WatchDirs.resolve projectRoot projectFiles cfgFile effectiveTargets
 
@@ -151,10 +135,7 @@
     warnMissingTargetPlaceholder "test" cfgFile.test.commandConfig.commandTemplate
     warnMissingTargetPlaceholder "eval" cfgFile.eval.commandTemplate
 
-    warnIgnoredextraAutoArguments
-        "build"
-        (cfgFile.build.commandTemplate <|> cfgFile.command)
-        cfgFile.build.extraAutoArguments
+    warnIgnoredextraAutoArguments "build" cfgFile.build.commandTemplate cfgFile.build.extraAutoArguments
     warnIgnoredextraAutoArguments
         "test"
         cfgFile.test.commandConfig.commandTemplate
@@ -168,9 +149,7 @@
             , evalSession
             , watchDirs
             , watchExclusionPatterns
-            , testMemoryLimit
             , replBuildDir = ReplBuildDir cfgFile.replBuildDir
-            , testTimeout = TestTimeout cfgFile.testTimeout
             , generateWithHpack = GenerateWithHpack cfgFile.generateWithHpack
             , hooks = fromMaybe def cfgFile.hooks
             , idleTimeout = IdleTimeout $ fromIntegral cfgFile.idleTimeoutSeconds
@@ -245,9 +224,6 @@
                 (toText . showPattern . fst)
                 session.watchExclusionPatterns.getWatchExclusionPatterns
         , "Repl build dir: " <> toText session.replBuildDir.getReplBuildDir
-        , -- TODO: Remove " seconds" when TestTimeout is converted to a proper time unit.
-          "Test timeout: " <> show session.testTimeout.getTestTimeout <> " seconds"
-        , "Test memory limit: " <> show session.testMemoryLimit
         , "Generate with hpack: " <> show session.generateWithHpack.getGenerateWithHpack
         , "Hooks:"
         , indent $ Hooks.display session.hooks
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
@@ -128,7 +128,7 @@
     => Eff es (Either Text [FilePath])
 discoverCabalPackages = do
     ProjectRoot projectRoot <- Reader.ask
-    homeCabalFiles <- maybe [] (one . (</> ".cabal/config")) <$> Env.lookupEnv "HOME"
+    homeCabalFiles <- maybe [] (one . (</> ".cabal/config")) <$> Env.lookup "HOME"
     let projectFilePaths = projectCabalFiles projectRoot <> homeCabalFiles
     projectFiles <- filterM doesFileExist projectFilePaths
     if null projectFiles
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
@@ -1,7 +1,8 @@
 module Tricorder.Session.Config (Config (..)) where
 
 import Atelier.Types.JSON.QuietSnake (QuietSnake (..))
-import Atelier.Types.WithDefaults (WithDefaults (..))
+import Atelier.Types.JSON.Renamed (Renamed)
+import Atelier.Types.JSON.WithDefaults (WithDefaults (..))
 import Data.Aeson (FromJSON (..), ToJSON)
 import Data.Default (Default (..))
 
@@ -19,20 +20,14 @@
     , watchDirs :: [FilePath]
     , watchExclusionPatterns :: [Text]
     , replBuildDir :: FilePath
-    , testTimeout :: Int
+    , testTimeout :: Maybe (Renamed "test_timeout" "test.timeout_ms")
+    , testMemoryLimit :: Maybe (Renamed "test_memory_limit" "test.memory_limit")
     , generateWithHpack :: Bool
-    , testMemoryLimit :: Maybe Text
     , hooks :: Maybe Hooks
     , idleTimeoutSeconds :: Int
-    , command :: Maybe Text
-    -- ^ DEPRECATED: use 'build'.'commandTemplate' instead.
-    -- TODO: Remove at or after version 0.8.0.0.
-    , targets :: [Text]
-    -- ^ DEPRECATED: use 'build'.'targets' instead.
-    -- TODO: Remove at or after version 0.8.0.0.
-    , testTargets :: Maybe [Text]
-    -- ^ DEPRECATED: use 'test'.'targets' instead.
-    -- TODO: Remove at or after version 0.8.0.0.
+    , command :: Maybe (Renamed "command" "build.command_template")
+    , targets :: Maybe (Renamed "targets" "build.targets")
+    , testTargets :: Maybe (Renamed "test_targets" "test.targets")
     }
     deriving stock (Eq, Generic, Show)
     deriving (ToJSON) via QuietSnake Config
@@ -48,12 +43,12 @@
             , watchDirs = []
             , watchExclusionPatterns = []
             , replBuildDir = "dist-newstyle/tricorder"
-            , testTimeout = 10
-            , generateWithHpack = True
+            , testTimeout = Nothing
             , testMemoryLimit = Nothing
+            , generateWithHpack = True
             , hooks = Nothing
             , idleTimeoutSeconds = 300
             , command = Nothing
-            , targets = []
+            , targets = Nothing
             , testTargets = Nothing
             }
diff --git a/src/Tricorder/Session/Hooks.hs b/src/Tricorder/Session/Hooks.hs
--- a/src/Tricorder/Session/Hooks.hs
+++ b/src/Tricorder/Session/Hooks.hs
@@ -9,7 +9,7 @@
 where
 
 import Atelier.Types.JSON.QuietSnake (QuietSnake (..))
-import Atelier.Types.WithDefaults (WithDefaults (..))
+import Atelier.Types.JSON.WithDefaults (WithDefaults (..))
 import Data.Aeson (FromJSON, ToJSON)
 import Data.Default (Default (..))
 
diff --git a/src/Tricorder/Session/Stage/Build/Command.hs b/src/Tricorder/Session/Stage/Build/Command.hs
--- a/src/Tricorder/Session/Stage/Build/Command.hs
+++ b/src/Tricorder/Session/Stage/Build/Command.hs
@@ -34,22 +34,20 @@
     -> [Target]
     -> Eff es (CommandTemplate 'Build, [Target])
 resolve projectRoot cfg repl effectiveTargets = do
-    template <- case customTemplate of
+    template <- case cfg.build.commandTemplate of
         Just tpl -> pure tpl
         Nothing -> defaultBuildTemplate projectRoot repl cfg.replBuildDir
     pure
         ( CommandTemplate
             { repl
             , template
-            , arguments = maybe cfg.build.extraAutoArguments (const []) customTemplate
+            , arguments = maybe cfg.build.extraAutoArguments (const []) cfg.build.commandTemplate
             , placeholder = targetsPlaceholder
             }
         , if not (null effectiveTargets)
             then effectiveTargets
             else [Bare "all"]
         )
-  where
-    customTemplate = cfg.build.commandTemplate <|> cfg.command
 
 
 -- | Whether the project has a @cabal.project@ or any @*.cabal@ file. Used
diff --git a/src/Tricorder/Session/Stage/Build/Session.hs b/src/Tricorder/Session/Stage/Build/Session.hs
--- a/src/Tricorder/Session/Stage/Build/Session.hs
+++ b/src/Tricorder/Session/Stage/Build/Session.hs
@@ -57,7 +57,7 @@
             , ghci = maybe def Ghci.Config.File.toConfig config.build.ghci
             }
   where
-    rawBuildTargets = fromMaybe config.targets config.build.targets
+    rawBuildTargets = fromMaybe [] config.build.targets
     effectiveTargets = Target.resolve projectFiles rawBuildTargets
 
 
diff --git a/src/Tricorder/Session/Stage/Test/Command.hs b/src/Tricorder/Session/Stage/Test/Command.hs
--- a/src/Tricorder/Session/Stage/Test/Command.hs
+++ b/src/Tricorder/Session/Stage/Test/Command.hs
@@ -4,7 +4,6 @@
     )
 where
 
-import Tricorder.Build.ByteSize (ByteSize)
 import Tricorder.Session.Command.RenderedCommand (RenderedCommand (..))
 import Tricorder.Session.CommandTemplate (CommandTemplate (..))
 import Tricorder.Session.Repl (Repl (..))
@@ -22,8 +21,8 @@
     }
 
 
-render :: Session -> Maybe ByteSize -> TestTarget -> RenderedTestCommand
-render testSession mMemoryLimit target =
+render :: Session -> TestTarget -> RenderedTestCommand
+render testSession target =
     RenderedTestCommand
         { command =
             RenderedCommand
@@ -57,4 +56,4 @@
                         Cabal -> cabal
                         Unknown -> cabal
             )
-            mMemoryLimit
+            testSession.options.memoryLimit
diff --git a/src/Tricorder/Session/Stage/Test/Config.hs b/src/Tricorder/Session/Stage/Test/Config.hs
--- a/src/Tricorder/Session/Stage/Test/Config.hs
+++ b/src/Tricorder/Session/Stage/Test/Config.hs
@@ -5,6 +5,7 @@
     )
 where
 
+import Atelier.Time (AsRawUnit, Millisecond)
 import Atelier.Types.JSON.QuietSnake (QuietSnake (..))
 import Data.Aeson (FromJSON (..), ToJSON, Value (..))
 import Data.Aeson.Types (ToJSON (..))
@@ -41,8 +42,10 @@
     parseJSON v = TestConfig <$> parseJSON v <*> parseJSON v
 
 
-newtype Options = Options
+data Options = Options
     { outputMode :: Maybe OutputMode
+    , timeoutMs :: Maybe (AsRawUnit Millisecond)
+    , memoryLimit :: Maybe Text
     }
     deriving stock (Eq, Generic, Show)
     deriving (FromJSON, ToJSON) via QuietSnake Options
@@ -52,6 +55,8 @@
     def =
         Options
             { outputMode = Nothing
+            , timeoutMs = Nothing
+            , memoryLimit = Nothing
             }
 
 
diff --git a/src/Tricorder/Session/Stage/Test/Session.hs b/src/Tricorder/Session/Stage/Test/Session.hs
--- a/src/Tricorder/Session/Stage/Test/Session.hs
+++ b/src/Tricorder/Session/Stage/Test/Session.hs
@@ -7,10 +7,15 @@
     )
 where
 
+import Atelier.Effects.Log (Log)
+import Atelier.Time (Millisecond)
 import Data.Default (Default (..))
 
+import Atelier.Effects.Log qualified as Log
+import Atelier.Time qualified as Time
 import Data.Text qualified as T
 
+import Tricorder.Build.ByteSize (ByteSize)
 import Tricorder.Session.CommandConfig (CommandConfig (..))
 import Tricorder.Session.CommandTemplate (CommandTemplate (..), targetPlaceholder)
 import Tricorder.Session.Config (Config (..))
@@ -20,6 +25,7 @@
 import Tricorder.Session.TestTarget (TestTarget (..))
 import Tricorder.Session.Util (displayList, indent)
 
+import Tricorder.Build.ByteSize qualified as ByteSize
 import Tricorder.Daemon.Ghci.Config qualified as Ghci (Config)
 import Tricorder.Daemon.Ghci.Config qualified as Ghci.Config
 import Tricorder.Daemon.Ghci.Config.File qualified as Ghci.Config.File
@@ -49,35 +55,54 @@
 
 data ResolvedTestOptions = ResolvedTestOptions
     { outputMode :: OutputMode
+    , timeout :: Millisecond
+    , memoryLimit :: Maybe ByteSize
     }
     deriving stock (Eq)
 
 
 instance Default ResolvedTestOptions where
-    def = ResolvedTestOptions ReplOutput
+    def =
+        ResolvedTestOptions
+            { outputMode = ReplOutput
+            , timeout = 10
+            , memoryLimit = Nothing
+            }
 
 
-resolve :: Repl -> [Target] -> Config -> Session
-resolve repl buildTargets cfg =
-    Session
-        { commandTemplate =
-            CommandTemplate
-                { repl
-                , template
-                , arguments =
-                    maybe
-                        cfg.test.commandConfig.extraAutoArguments
-                        (const [])
-                        cfg.test.commandConfig.commandTemplate
-                , placeholder = targetPlaceholder
-                }
-        , targets = TestTarget.resolve cfg buildTargets
-        , options =
-            ResolvedTestOptions
-                { outputMode = fromMaybe detectedOutputMode cfg.test.options.outputMode
-                }
-        , ghci = maybe def Ghci.Config.File.toConfig cfg.test.commandConfig.ghci
-        }
+resolve :: (Log :> es) => Repl -> [Target] -> Config -> Eff es Session
+resolve repl buildTargets cfg = Log.withNamespace "session.test" do
+    memoryLimit <- case cfg.test.options.memoryLimit of
+        Nothing -> pure Nothing
+        Just limit -> case ByteSize.fromText limit of
+            Nothing -> do
+                Log.warn $ "Unable to parse options.memory_limit: " <> limit
+                Log.info "Continuing with no memory limit."
+                pure Nothing
+            Just parsedLimit ->
+                pure $ Just parsedLimit
+    pure
+        $ Session
+            { commandTemplate =
+                CommandTemplate
+                    { repl
+                    , template
+                    , arguments =
+                        maybe
+                            cfg.test.commandConfig.extraAutoArguments
+                            (const [])
+                            cfg.test.commandConfig.commandTemplate
+                    , placeholder = targetPlaceholder
+                    }
+            , targets = TestTarget.resolve cfg buildTargets
+            , options =
+                ResolvedTestOptions
+                    { outputMode = fromMaybe detectedOutputMode cfg.test.options.outputMode
+                    , timeout = maybe 10 Time.getAsRawUnit cfg.test.options.timeoutMs
+                    , memoryLimit
+                    }
+            , ghci = maybe def Ghci.Config.File.toConfig cfg.test.commandConfig.ghci
+            }
   where
     template = fromMaybe (defaultTestTemplate repl) cfg.test.commandConfig.commandTemplate
     detectedOutputMode
@@ -114,4 +139,6 @@
         T.intercalate
             "\n"
             [ "Output mode: " <> show opts.outputMode
+            , "Timeout: " <> show opts.timeout
+            , "Memory limit: " <> maybe "<unlimited>" show opts.memoryLimit
             ]
diff --git a/src/Tricorder/Session/State.hs b/src/Tricorder/Session/State.hs
--- a/src/Tricorder/Session/State.hs
+++ b/src/Tricorder/Session/State.hs
@@ -27,6 +27,7 @@
 import Atelier.Effects.Conc qualified as Conc
 import Atelier.Effects.FileSystem qualified as FileSystem
 import Atelier.Effects.FileWatcher qualified as FileWatcher
+import Atelier.Effects.FileWatcher.Watch qualified as Watch
 import Atelier.Effects.Input qualified as Input
 import Atelier.Effects.Log qualified as Log
 import Atelier.Effects.PubSub qualified as PubSub
@@ -136,7 +137,7 @@
 watchConfigFile = do
     root <- Reader.ask
     FileWatcher.watchFilePathsDebounced
-        [FileWatcher.dirWhere root.getProjectRoot (LoadedConfig.configFileName `List.isSuffixOf`)]
+        [Watch.dirWhere root.getProjectRoot (LoadedConfig.configFileName `List.isSuffixOf`)]
         \_ _ -> Pub.publish Reload
 
 
@@ -154,7 +155,7 @@
     exists <- FileSystem.doesFileExist $ root.getProjectRoot </> "stack.yaml"
     when exists do
         Conc.fork_ $ FileWatcher.watchFilePathsDebounced
-            [FileWatcher.dirWhere root.getProjectRoot ("stack.yaml" `List.isSuffixOf`)]
+            [Watch.dirWhere root.getProjectRoot ("stack.yaml" `List.isSuffixOf`)]
             \_ _ -> Pub.publish Reload
 
 
diff --git a/src/Tricorder/Session/TestTarget.hs b/src/Tricorder/Session/TestTarget.hs
--- a/src/Tricorder/Session/TestTarget.hs
+++ b/src/Tricorder/Session/TestTarget.hs
@@ -50,6 +50,6 @@
 -- ever names test suites [ref:test_targets_invariant]. With neither source
 -- set, falls back to deriving test targets from the build 'targets'.
 resolve :: Config -> [Target] -> [TestTarget]
-resolve cfg targets = case cfg.test.commandConfig.targets <|> cfg.testTargets of
+resolve cfg targets = case cfg.test.commandConfig.targets of
     Just explicit -> mapMaybe parse explicit
     Nothing -> mapMaybe project targets
diff --git a/src/Tricorder/Session/TestTimeout.hs b/src/Tricorder/Session/TestTimeout.hs
deleted file mode 100644
--- a/src/Tricorder/Session/TestTimeout.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Tricorder.Session.TestTimeout (TestTimeout (..)) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Default (Default (..))
-
-
-newtype TestTimeout = TestTimeout {getTestTimeout :: Int}
-    deriving stock (Eq, Generic, Show)
-    deriving (FromJSON, ToJSON) via Int
-
-
-instance Default TestTimeout where
-    def = TestTimeout 10
diff --git a/src/Tricorder/Socket/Client.hs b/src/Tricorder/Socket/Client.hs
--- a/src/Tricorder/Socket/Client.hs
+++ b/src/Tricorder/Socket/Client.hs
@@ -20,14 +20,14 @@
 import Effectful.Reader.Static (Reader, ask)
 import Effectful.State.Static.Shared (evalState, get, modify, put)
 import System.IO.Error (isEOFError)
-import Tricorder.API.Force (Force)
+import Tricorder.API.IfThereArePendingQueries (IfThereArePendingQueries)
 import Prelude hiding (force)
 
 import Atelier.Effects.Delay qualified as Delay
 import Atelier.Effects.File qualified as File
 import Atelier.Effects.Posix.Daemons qualified as Daemons
 import Data.ByteString.Lazy qualified as BSL
-import Tricorder.API.Force qualified as Force
+import Tricorder.API.IfThereArePendingQueries qualified as IfThereArePendingQueries
 
 import Tricorder.Build (BuildState, Diagnostic)
 import Tricorder.Runtime (PidFile)
@@ -126,7 +126,7 @@
 
 requestShutdown
     :: (File :> es, UnixSocket :> es)
-    => Force -> FilePath -> Eff es (Either Text ())
+    => IfThereArePendingQueries -> FilePath -> Eff es (Either Text ())
 requestShutdown force sockPath = withConnection sockPath \h -> do
     sendQuery h $ Quit waiters
     line <- File.hGetLine h
@@ -137,8 +137,8 @@
             pure $ Left "Failed to request shutdown"
   where
     waiters = case force of
-        Force.Force -> IgnoreWaiters
-        Force.NoForce -> WaitForWaiters
+        IfThereArePendingQueries.DontWait -> IgnoreWaiters
+        IfThereArePendingQueries.Wait -> WaitForWaiters
 
 
 isDaemonRunning :: (Daemons :> es, Reader PidFile :> es) => Eff es Bool
diff --git a/src/Tricorder/Socket/Server.hs b/src/Tricorder/Socket/Server.hs
--- a/src/Tricorder/Socket/Server.hs
+++ b/src/Tricorder/Socket/Server.hs
@@ -1,7 +1,7 @@
 module Tricorder.Socket.Server (main, SocketRemoved (..)) where
 
 import Atelier.Effects.Conc (Conc)
-import Atelier.Effects.Exit (Exit, exitSuccess)
+import Atelier.Effects.Exit (Exit)
 import Atelier.Effects.File (File)
 import Atelier.Effects.FileSystem (FileSystem)
 import Atelier.Effects.Input (Input, input)
@@ -14,6 +14,7 @@
 import System.IO (Handle)
 
 import Atelier.Effects.Conc qualified as Conc
+import Atelier.Effects.Exit qualified as Exit
 import Atelier.Effects.File qualified as File
 import Atelier.Effects.FileSystem qualified as FileSystem
 import Atelier.Effects.Log qualified as Log
@@ -154,7 +155,7 @@
     doQuit = do
         sendJson h True
         Log.info "Shutting down."
-        exitSuccess
+        Exit.success
 
 
 respondOnce
diff --git a/src/Tricorder/SourceLookup.hs b/src/Tricorder/SourceLookup.hs
--- a/src/Tricorder/SourceLookup.hs
+++ b/src/Tricorder/SourceLookup.hs
@@ -6,7 +6,7 @@
 where
 
 import Atelier.Effects.FileSystem (FileSystem, doesFileExist, readFileLbs)
-import Atelier.Effects.Input (Input, input)
+import Atelier.Effects.Input (Input)
 import Atelier.Effects.Log (Log)
 import System.FilePath (normalise, takeDirectory, (</>))
 import Tricorder.SourceLookup.ModuleSourceResult (ModuleSourceResult (..))
@@ -14,13 +14,15 @@
 import Tricorder.SourceLookup.ReexportLimits (ReexportLimits (..))
 import Tricorder.SourceLookup.SourceQuery (ModuleName (..), SourceQuery (..))
 
+import Atelier.Effects.Input qualified as Input
 import Atelier.Effects.Log qualified as Log
 import Data.Set qualified as Set
 import Data.Text qualified as T
 import Tricorder.SourceLookup.ModuleSourceResult qualified as ModuleSourceResult
 
 import Tricorder.Session.CabalFile (CabalFile (..))
-import Tricorder.Session.Repl (Repl)
+import Tricorder.Session.CommandTemplate (CommandTemplate (..))
+import Tricorder.Session.Stage.Build.Session (Session (..))
 import Tricorder.Session.WatchDirs (allSourceDirs)
 import Tricorder.SourceLookup.GhcPkg (GhcPkg)
 import Tricorder.SourceLookup.Hackage (Hackage)
@@ -34,6 +36,7 @@
     , sourceExtensions
     )
 
+import Tricorder.Session.Stage.Build.Session qualified as Build (Session)
 import Tricorder.SourceLookup.GhcPkg qualified as GhcPkg
 
 
@@ -83,7 +86,7 @@
     :: ( FileSystem :> es
        , GhcPkg :> es
        , Hackage :> es
-       , Input Repl :> es
+       , Input Build.Session :> es
        , Log :> es
        , PackageStore :> es
        )
@@ -111,7 +114,7 @@
     :: ( FileSystem :> es
        , GhcPkg :> es
        , Hackage :> es
-       , Input Repl :> es
+       , Input Build.Session :> es
        , Log :> es
        , PackageStore :> es
        )
@@ -180,7 +183,7 @@
     :: ( FileSystem :> es
        , GhcPkg :> es
        , Hackage :> es
-       , Input Repl :> es
+       , Input Build.Session :> es
        , Log :> es
        , PackageStore :> es
        )
@@ -238,14 +241,14 @@
 -- | Resolve a module to its package, consulting the module -> package cache first.
 resolvePackage
     :: ( GhcPkg :> es
-       , Input Repl :> es
+       , Input Build.Session :> es
        , Log :> es
        )
     => ModuleName
     -> Eff es (Maybe PackageId)
 resolvePackage modName = do
-    repl <- input
-    result <- GhcPkg.findModule repl modName
+    session <- Input.input @Build.Session
+    result <- GhcPkg.findModule session.commandTemplate.repl modName
     Log.debug $ "Source: find-module " <> unModuleName modName <> " → " <> show result
     pure result
 
diff --git a/src/Tricorder/SourceLookup/PackageStore.hs b/src/Tricorder/SourceLookup/PackageStore.hs
--- a/src/Tricorder/SourceLookup/PackageStore.hs
+++ b/src/Tricorder/SourceLookup/PackageStore.hs
@@ -6,7 +6,7 @@
     )
 where
 
-import Atelier.Effects.Env (Env, getEnvironment)
+import Atelier.Effects.Env (Env)
 import Atelier.Effects.FileSystem (FileSystem)
 import Effectful (Effect)
 import Effectful.Dispatch.Dynamic (interpretWith_)
@@ -15,6 +15,7 @@
 import System.FilePath (takeDirectory, (</>))
 import Tricorder.SourceLookup.PackageId (PackageId (..), splitPackageId)
 
+import Atelier.Effects.Env qualified as Env
 import Atelier.Effects.FileSystem qualified as FileSystem
 import Data.Map.Strict qualified as Map
 
@@ -66,7 +67,7 @@
 
 findStoreBaseDir :: (Env :> es, FileSystem :> es) => Eff es FilePath
 findStoreBaseDir = do
-    env <- Map.fromList <$> getEnvironment
+    env <- Map.fromList <$> Env.all
     fmap (fromMaybe tempFallback . rightToMaybe)
         $ runNonDet OnEmptyKeep
         $ findCabalDirCandidate env
diff --git a/test/Unit/Tricorder/Daemon/Ghci/ProcessSpec.hs b/test/Unit/Tricorder/Daemon/Ghci/ProcessSpec.hs
--- a/test/Unit/Tricorder/Daemon/Ghci/ProcessSpec.hs
+++ b/test/Unit/Tricorder/Daemon/Ghci/ProcessSpec.hs
@@ -1,12 +1,8 @@
 module Unit.Tricorder.Daemon.Ghci.ProcessSpec (test_Process) where
 
-import Atelier.Effects.Conc (runConc)
-import Atelier.Effects.Delay (runDelay)
-import Atelier.Effects.File (runFile)
-import Atelier.Effects.Log (Message (..), Severity (..), runLogNoOp, runLogWriter)
-import Atelier.Effects.Process (runProcessIO, terminateProcessGroup, withProcessGroup)
+import Atelier.Effects.Log (Message (..), Severity (..))
+import Atelier.Effects.Process (terminateProcessGroup, withProcessGroup)
 import Atelier.Effects.Process.Internal (RunningProcess (..))
-import Atelier.Effects.Timeout (runTimeout)
 import Atelier.Time (Millisecond)
 import Control.Concurrent (threadDelay)
 import Control.Concurrent.STM (newTVarIO)
@@ -40,7 +36,10 @@
 import Atelier.Effects.Conc qualified as Conc
 import Atelier.Effects.Delay qualified as Delay
 import Atelier.Effects.File qualified as File
+import Atelier.Effects.Log qualified as Log
 import Atelier.Effects.Process qualified as AProc
+import Atelier.Effects.Process qualified as Atelier.Process
+import Atelier.Effects.Timeout qualified as Timeout
 import Data.Text qualified as T
 import System.Process qualified as Process
 import System.Timeout qualified
@@ -81,8 +80,8 @@
         (result, _msgs) <-
             runEff
                 . runWriter @[Message]
-                . runLogWriter
-                . runFile
+                . Log.runWriter
+                . File.run
                 $ do
                     for_ ["line1", "line2", finishMarker 1, "line3"] (File.hPutTextLn w)
                     File.hClose w
@@ -94,8 +93,8 @@
         (result, _msgs) <-
             runEff
                 . runWriter @[Message]
-                . runLogWriter
-                . runFile
+                . Log.runWriter
+                . File.run
                 $ do
                     for_ ["a", "b", "c", finishMarker 2] (File.hPutTextLn w)
                     File.hClose w
@@ -108,8 +107,8 @@
         (result, _msgs) <-
             runEff
                 . runWriter @[Message]
-                . runLogWriter
-                . runFile
+                . Log.runWriter
+                . File.run
                 $ do
                     -- 'finishMarker 5' is a leftover from a prior, interrupted
                     -- command; the drain waiting for 'finishMarker 9' must
@@ -125,8 +124,8 @@
             (outcome, _msgs) <-
                 runEff
                     . runWriter @[Message]
-                    . runLogWriter
-                    . runFile
+                    . Log.runWriter
+                    . File.run
                     $ do
                         for_ ["first", "second", "third"] (File.hPutTextLn w)
                         File.hClose w -- EOF before the marker ever arrives
@@ -144,8 +143,8 @@
         (outcome, _msgs) <-
             runEff
                 . runWriter @[Message]
-                . runLogWriter
-                . runFile
+                . Log.runWriter
+                . File.run
                 $ do
                     File.hClose w
                     trySync (Process.drainUntil r (finishMarker 1) (\_ -> pure ()))
@@ -170,8 +169,8 @@
             (_outcome, msgs) <-
                 runEff
                     . runWriter @[Message]
-                    . runLogWriter
-                    . runFile
+                    . Log.runWriter
+                    . File.run
                     $ do
                         File.hClose w
                         trySync (Process.drainUntil r (finishMarker 3) (\_ -> pure ()))
@@ -185,8 +184,8 @@
         (_outcome, msgs) <-
             runEff
                 . runWriter @[Message]
-                . runLogWriter
-                . runFile
+                . Log.runWriter
+                . File.run
                 $ do
                     for_ ["oops-line-1", "oops-line-2"] (File.hPutTextLn w)
                     File.hClose w
@@ -235,11 +234,11 @@
         result <-
             runEff
                 . runConcurrent
-                . runTimeout
-                . runDelay
-                . runFile
-                . runLogNoOp
-                . runConc
+                . Timeout.run
+                . Delay.runIO
+                . File.run
+                . Log.runNoOp
+                . Conc.runIO
                 $ do
                     -- A stale 'marker 5' (left by a prior interrupted reload)
                     -- precedes the fresh command's real output + its 'marker 9'
@@ -294,11 +293,11 @@
         written <-
             runEff
                 . runConcurrent
-                . runTimeout
-                . runDelay
-                . runFile
-                . runLogNoOp
-                . runConc
+                . Timeout.run
+                . Delay.runIO
+                . File.run
+                . Log.runNoOp
+                . Conc.runIO
                 $ do
                     -- Pre-seed the marker on both streams so the drain returns
                     -- immediately; we only care about what was written to stdin.
@@ -339,10 +338,10 @@
         result <-
             runEff
                 . runConcurrent
-                . runTimeout
-                . runDelay
-                . runFile
-                . runConc
+                . Timeout.run
+                . Delay.runIO
+                . File.run
+                . Conc.runIO
                 $ do
                     -- No banner will ever arrive: close the write end so the
                     -- wait sees EOF at once and takes the "command exited"
@@ -383,11 +382,11 @@
         let scenario =
                 runEff
                     . runConcurrent
-                    . runTimeout
-                    . runDelay
-                    . runFile
-                    . runConc
-                    . runProcessIO
+                    . Timeout.run
+                    . Delay.runIO
+                    . File.run
+                    . Conc.runIO
+                    . Atelier.Process.run
                     $ withProcessGroup procConfig \p -> do
                         -- First stdout line is the long-lived child's pid.
                         line <- File.hGetLine (AProc.getStdout p)
@@ -439,7 +438,7 @@
                     ignoring (stopProcess p)
                     assertFailure ("could not parse child pid from: " <> show childLine)
                 Just childPid -> do
-                    runEff . runProcessIO $ terminateProcessGroup (RunningProcess p)
+                    runEff . Atelier.Process.run $ terminateProcessGroup (RunningProcess p)
                     died <- waitForProcessDeath childPid
                     -- Never leak the child if the assertion fails.
                     ignoring (signalProcess sigKILL (fromIntegral childPid))
@@ -532,11 +531,11 @@
         result <-
             runEff
                 . runConcurrent
-                . runTimeout
-                . runDelay
-                . runFile
-                . runLogNoOp
-                . runConc
+                . Timeout.run
+                . Delay.runIO
+                . File.run
+                . Log.runNoOp
+                . Conc.runIO
                 $ Conc.scoped do
                     -- A sibling fork in the SAME ambient scope. If the bug
                     -- regresses, the drain exception will tear this scope
diff --git a/test/Unit/Tricorder/Daemon/Ghci/SessionSpec.hs b/test/Unit/Tricorder/Daemon/Ghci/SessionSpec.hs
--- a/test/Unit/Tricorder/Daemon/Ghci/SessionSpec.hs
+++ b/test/Unit/Tricorder/Daemon/Ghci/SessionSpec.hs
@@ -149,4 +149,4 @@
     :: [Either SomeException LoadResult]
     -> Eff '[Session, Pub BuildProgress, IOE] a
     -> IO a
-runScripted results = runEff . Pub.runNoOp . Session.runScripted results
+runScripted results = runEff . Pub.noOp . Session.runScripted results
diff --git a/test/Unit/Tricorder/Daemon/TestRunnerSpec.hs b/test/Unit/Tricorder/Daemon/TestRunnerSpec.hs
--- a/test/Unit/Tricorder/Daemon/TestRunnerSpec.hs
+++ b/test/Unit/Tricorder/Daemon/TestRunnerSpec.hs
@@ -16,7 +16,6 @@
     )
 import Tricorder.Session.Command.RenderedCommand (RenderedCommand (..))
 import Tricorder.Session.Stage.Test.Command (RenderedTestCommand (..))
-import Tricorder.Session.TestTimeout (TestTimeout (..))
 
 import Tricorder.Build.Test qualified as Test
 import Tricorder.Daemon.TestRunner qualified as TestRunner
@@ -107,7 +106,7 @@
     [ testCase "returns scripted TestRun" do
         result <-
             runScripted [Right passingRun]
-                $ runTestSuite def noProgress testTimeout
+                $ runTestSuite def noProgress
                 $ mkResolved
                 $ RenderedCommand
                 $ "cabal repl test:foo"
@@ -115,7 +114,7 @@
     , testCase "ignores the target name argument" do
         result <-
             runScripted [Right failingRun]
-                $ runTestSuite def noProgress testTimeout
+                $ runTestSuite def noProgress
                 $ mkResolved
                 $ RenderedCommand
                 $ "cabal repl test:anything"
@@ -124,7 +123,7 @@
         result <-
             runScripted [Left (toException boom)]
                 $ try @ErrorCall
-                $ runTestSuite def noProgress testTimeout
+                $ runTestSuite def noProgress
                 $ mkResolved
                 $ RenderedCommand
                 $ "cabal repl test:foo"
@@ -134,12 +133,12 @@
         [ testCase "consumes results in order across multiple calls" do
             (a, b) <- runScripted [Right passingRun, Right failingRun] do
                 a <-
-                    runTestSuite def noProgress testTimeout
+                    runTestSuite def noProgress
                         $ mkResolved
                         $ RenderedCommand
                         $ "cabal repl test:foo"
                 b <-
-                    runTestSuite def noProgress testTimeout
+                    runTestSuite def noProgress
                         $ mkResolved
                         $ RenderedCommand
                         $ "cabal repl test:bar"
@@ -150,12 +149,12 @@
             result <- runScripted [Left (toException boom), Right passingRun] do
                 r1 <-
                     try @ErrorCall
-                        $ runTestSuite def noProgress testTimeout
+                        $ runTestSuite def noProgress
                         $ mkResolved
                         $ RenderedCommand
                         $ "cabal repl test:foo"
                 r2 <-
-                    runTestSuite def noProgress testTimeout
+                    runTestSuite def noProgress
                         $ mkResolved
                         $ RenderedCommand
                         $ "cabal repl test:bar"
@@ -202,10 +201,6 @@
 
 mkResolved :: RenderedCommand 'Stage.Test -> RenderedTestCommand
 mkResolved cmd = RenderedTestCommand cmd def
-
-
-testTimeout :: TestTimeout
-testTimeout = TestTimeout (-1)
 
 
 noProgress :: b -> Eff es ()
diff --git a/test/Unit/Tricorder/Daemon/WatchSpec.hs b/test/Unit/Tricorder/Daemon/WatchSpec.hs
--- a/test/Unit/Tricorder/Daemon/WatchSpec.hs
+++ b/test/Unit/Tricorder/Daemon/WatchSpec.hs
@@ -1,6 +1,7 @@
 module Unit.Tricorder.Daemon.WatchSpec (test_Watch) where
 
-import Atelier.Effects.FileWatcher (FileEvent (..), matchesAny)
+import Atelier.Effects.FileWatcher (matchesAny)
+import Atelier.Effects.FileWatcher.Event (Event (..))
 import Effectful (runEff)
 import Effectful.Writer.Static.Shared (execWriter, runWriter)
 import Test.Tasty (TestTree, testGroup)
@@ -9,10 +10,7 @@
 
 import Atelier.Effects.PubSub.Pub qualified as Pub
 
-import Tricorder.Build.Changes
-    ( CabalChangeDetected (..)
-    , SourceChangeDetected (..)
-    )
+import Tricorder.Build.Changes (CabalChangeDetected (..), SourceChangeDetected (..))
 import Tricorder.Daemon.Watch (WatchedFile (..))
 import Tricorder.Runtime (ProjectRoot (..))
 import Tricorder.Session.WatchDirs (WatchDirs (..))
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,16 +1,16 @@
 module Unit.Tricorder.Session.CabalFileSpec (test_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.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (testCase, (@?=))
 
+import Atelier.Effects.Env qualified as Env
+import Atelier.Effects.FileSystem qualified as FileSystem
 import Atelier.Effects.FileSystem.Glob qualified as Glob
 import Atelier.Effects.Input qualified as Input
+import Atelier.Effects.Log qualified as Log
 import Data.Map.Strict qualified as Map
 
 import Tricorder.Runtime (ProjectRoot (..))
@@ -156,9 +156,9 @@
     runDiscovery fs env = runDiscoveryGlob fs env []
     runDiscoveryGlob fs env script =
         runPureEff
-            . runEnvConst env
+            . Env.runConst env
             . evalState fs
-            . runFileSystemState
+            . FileSystem.runState
             . Glob.runScripted script
             . runReader pr
 
@@ -182,7 +182,7 @@
     runStack result =
         runPureEff
             . runReader pr
-            . runLogNoOp
+            . Log.runNoOp
             . Input.withConst result
 
 
@@ -206,4 +206,4 @@
         fmap (.projectFilePath)
             . runPureEff
             . evalState fs
-            . runFileSystemState
+            . FileSystem.runState
diff --git a/test/Unit/Tricorder/Session/ReplSpec.hs b/test/Unit/Tricorder/Session/ReplSpec.hs
--- a/test/Unit/Tricorder/Session/ReplSpec.hs
+++ b/test/Unit/Tricorder/Session/ReplSpec.hs
@@ -1,11 +1,12 @@
 module Unit.Tricorder.Session.ReplSpec (test_Repl) where
 
-import Atelier.Effects.FileSystem (FileSystem, runFileSystemState)
+import Atelier.Effects.FileSystem (FileSystem)
 import Effectful (runPureEff)
 import Effectful.State.Static.Shared (State, evalState)
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (testCase, (@?=))
 
+import Atelier.Effects.FileSystem qualified as FileSystem
 import Data.Map.Strict qualified as Map
 
 import Tricorder.Runtime (ProjectRoot (..))
@@ -45,7 +46,7 @@
 -- @stack.yaml@, which is parsed for its @packages@ key).
 withFiles :: [(FilePath, ByteString)] -> Eff '[FileSystem, State (Map FilePath ByteString)] a -> a
 withFiles files action =
-    runPureEff $ evalState (Map.fromList files) $ runFileSystemState action
+    runPureEff $ evalState (Map.fromList files) $ FileSystem.runState action
 
 
 pr :: ProjectRoot
diff --git a/test/Unit/Tricorder/Session/Stage/Build/CommandSpec.hs b/test/Unit/Tricorder/Session/Stage/Build/CommandSpec.hs
--- a/test/Unit/Tricorder/Session/Stage/Build/CommandSpec.hs
+++ b/test/Unit/Tricorder/Session/Stage/Build/CommandSpec.hs
@@ -1,12 +1,13 @@
 module Unit.Tricorder.Session.Stage.Build.CommandSpec (test_Command) where
 
-import Atelier.Effects.FileSystem (FileSystem, runFileSystemState)
+import Atelier.Effects.FileSystem (FileSystem)
 import Data.Default (def)
 import Effectful (runPureEff)
 import Effectful.State.Static.Shared (State, evalState)
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (testCase, (@?=))
 
+import Atelier.Effects.FileSystem qualified as FileSystem
 import Data.Map.Strict qualified as Map
 
 import Tricorder.Runtime (ProjectRoot (..))
@@ -74,22 +75,6 @@
 testResolveBuildCommand :: [TestTree]
 testResolveBuildCommand =
     [ testGroup
-        "deprecated top-level command"
-        [ testCase "is used as the build template when build.command_template is unset" do
-            renderBuildFor [] def {command = Just "foo"} [] @?= "foo"
-        ]
-    , testGroup
-        "build.command_template"
-        [ testCase "overrides the deprecated top-level command" do
-            let cfg =
-                    cfg0
-                        { command = Just "should be ignored"
-                        , build = cfg0.build {commandTemplate = Just "cabal repl {targets}"}
-                        }
-            renderBuildFor [("/cabal.project", "")] cfg (Target.parse <$> ["lib:foo"])
-                @?= "cabal repl lib:foo"
-        ]
-    , testGroup
         "explicit targets"
         [ testCase "spell them out verbatim" do
             renderBuildFor [("/cabal.project", "")] cfg0 (Target.parse <$> ["lib:foo"])
@@ -145,14 +130,6 @@
                         }
             renderBuildFor [("/cabal.project", "")] cfg (Target.parse <$> ["lib:foo"])
                 @?= "cabal repl lib:foo"
-        , testCase "is ignored when the deprecated top-level command is set" do
-            let cfg =
-                    cfg0
-                        { command = Just "cabal repl {targets}"
-                        , build = cfg0.build {extraAutoArguments = ["--extra-flag"]}
-                        }
-            renderBuildFor [("/cabal.project", "")] cfg (Target.parse <$> ["lib:foo"])
-                @?= "cabal repl lib:foo"
         ]
     ]
 
@@ -171,7 +148,7 @@
 -- @stack.yaml@, which is parsed for its @packages@ key).
 withFiles :: [(FilePath, ByteString)] -> Eff '[FileSystem, State (Map FilePath ByteString)] a -> a
 withFiles files action =
-    runPureEff $ evalState (Map.fromList files) $ runFileSystemState action
+    runPureEff $ evalState (Map.fromList files) $ FileSystem.runState action
 
 
 cfg0 :: Config
diff --git a/test/Unit/Tricorder/Session/Stage/Test/CommandSpec.hs b/test/Unit/Tricorder/Session/Stage/Test/CommandSpec.hs
--- a/test/Unit/Tricorder/Session/Stage/Test/CommandSpec.hs
+++ b/test/Unit/Tricorder/Session/Stage/Test/CommandSpec.hs
@@ -1,25 +1,26 @@
 module Unit.Tricorder.Session.Stage.Test.CommandSpec (test_Command) where
 
 import Data.Default (def)
+import Effectful (runPureEff)
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (testCase, (@?=))
 
+import Atelier.Effects.Log qualified as Log
+
 import Tricorder.Build.ByteSize (ByteSize (..), Unit (..))
 import Tricorder.Session.Command.RenderedCommand (RenderedCommand (..))
 import Tricorder.Session.CommandConfig (CommandConfig (..))
 import Tricorder.Session.CommandTemplate (CommandTemplate (..), targetPlaceholder)
 import Tricorder.Session.Config (Config (..))
 import Tricorder.Session.Repl (Repl (..))
-import Tricorder.Session.Stage.Test.Command
-    ( RenderedTestCommand (..)
-    , render
-    )
+import Tricorder.Session.Stage.Test.Command (RenderedTestCommand (..), render)
 import Tricorder.Session.Stage.Test.Config (TestConfig (..))
-import Tricorder.Session.Stage.Test.Session (Session (..), resolve)
+import Tricorder.Session.Stage.Test.Session (ResolvedTestOptions (..), Session (..))
 import Tricorder.Session.TestTarget (TestTarget (..))
 
 import Tricorder.Session.CommandConfig qualified as CommandConfig
 import Tricorder.Session.Stage qualified as Stage
+import Tricorder.Session.Stage.Test.Session qualified as Session
 import Tricorder.Session.Target qualified as Target
 
 
@@ -61,7 +62,7 @@
   where
     test :: CommandTemplate 'Stage.Test -> Maybe ByteSize -> TestTarget -> Text
     test template mMemoryLimit target =
-        (render (Session template [] def def) mMemoryLimit target).command.getRenderedCommand
+        (render (Session template [] (def {memoryLimit = mMemoryLimit}) def) target).command.getRenderedCommand
     testTarget = TestTarget (Target.parse "test:foo")
     oneByte = ByteSize 1 B
 
@@ -104,3 +105,7 @@
                         }
             (resolve Cabal [] cfg).commandTemplate.arguments @?= []
         ]
+
+
+resolve :: Repl -> [Target.Target] -> Config -> Session
+resolve repl target config = runPureEff . Log.runNoOp $ Session.resolve repl target config
diff --git a/test/Unit/Tricorder/Session/TestTargetSpec.hs b/test/Unit/Tricorder/Session/TestTargetSpec.hs
--- a/test/Unit/Tricorder/Session/TestTargetSpec.hs
+++ b/test/Unit/Tricorder/Session/TestTargetSpec.hs
@@ -6,6 +6,9 @@
 
 import Tricorder.Session.Config (Config (..))
 
+import Tricorder.Session.CommandConfig qualified as CommandConfig
+import Tricorder.Session.Config qualified as Config
+import Tricorder.Session.Stage.Test.Config qualified as Test.Config
 import Tricorder.Session.Target qualified as Target
 import Tricorder.Session.TestTarget qualified as TestTarget
 
@@ -28,12 +31,30 @@
         let cfg = def :: Config
         TestTarget.resolve cfg (mkTargets ["lib:mylib", "exe:myapp"])
             @?= mapMaybe TestTarget.parse []
-    , testCase "uses explicit testTargets list when set" do
-        let cfg = def {testTargets = Just ["test:b-test"]} :: Config
+    , testCase "uses explicit test.commandConfig.targets list when set" do
+        let cfg =
+                def
+                    { Config.test =
+                        def
+                            { Test.Config.commandConfig =
+                                def
+                                    { CommandConfig.targets = Just ["test:b-test"]
+                                    }
+                            }
+                    }
         TestTarget.resolve cfg (mkTargets ["lib:a", "test:a-test", "test:b-test"])
             @?= mapMaybe TestTarget.parse ["test:b-test"]
     , testCase "returns empty list when testTargets is explicitly empty" do
-        let cfg = def {testTargets = Just []} :: Config
+        let cfg =
+                def
+                    { Config.test =
+                        def
+                            { Test.Config.commandConfig =
+                                def
+                                    { CommandConfig.targets = Just []
+                                    }
+                            }
+                    }
         TestTarget.resolve cfg (mkTargets ["lib:a", "test:a-test"])
             @?= []
     , testCase "infers multiple test: components" do
diff --git a/test/Unit/Tricorder/SessionSpec.hs b/test/Unit/Tricorder/SessionSpec.hs
--- a/test/Unit/Tricorder/SessionSpec.hs
+++ b/test/Unit/Tricorder/SessionSpec.hs
@@ -1,7 +1,6 @@
 module Unit.Tricorder.SessionSpec (test_Session) where
 
-import Atelier.Effects.FileSystem (runFileSystemState)
-import Atelier.Effects.Log (Message (..), Severity (..), runLogNoOp, runLogWriter)
+import Atelier.Effects.Log (Message (..), Severity (..))
 import Data.Aeson (object, (.=))
 import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe)
 import Effectful (runPureEff)
@@ -11,7 +10,9 @@
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (assertFailure, testCase, (@?), (@?=))
 
+import Atelier.Effects.FileSystem qualified as FileSystem
 import Atelier.Effects.Input qualified as Input
+import Atelier.Effects.Log qualified as Log
 import Data.Aeson qualified as Aeson
 import Data.Text qualified as T
 
@@ -78,9 +79,9 @@
         filter (\m -> m.severity /= DEBUG)
             . runPureEff
             . execWriter @[Message]
-            . runLogWriter
+            . Log.runWriter
             . evalState @(Map FilePath ByteString) mempty
-            . runFileSystemState
+            . FileSystem.runState
             . Input.withConst cabalFiles
             . runReader (ProjectRoot "/")
             . Input.withConst (LoadedConfig $ toStrict $ Aeson.encode cfg)
@@ -122,9 +123,9 @@
     captureLogsFor cfg =
         runPureEff
             . execWriter @[Message]
-            . runLogWriter
+            . Log.runWriter
             . evalState @(Map FilePath ByteString) mempty
-            . runFileSystemState
+            . FileSystem.runState
             . Input.withConst ([] :: [CabalFile])
             . runReader (ProjectRoot "/")
             . Input.withConst (LoadedConfig $ toStrict $ Aeson.encode cfg)
@@ -145,9 +146,9 @@
   where
     loadSessionWith cfg =
         runPureEff
-            . runLogNoOp
+            . Log.runNoOp
             . evalState @(Map FilePath ByteString) mempty
-            . runFileSystemState
+            . FileSystem.runState
             . Input.withConst ([] :: [CabalFile])
             . runReader (ProjectRoot "/")
             . Input.withConst (LoadedConfig $ toStrict $ Aeson.encode cfg)
diff --git a/test/Unit/Tricorder/SocketSpec.hs b/test/Unit/Tricorder/SocketSpec.hs
--- a/test/Unit/Tricorder/SocketSpec.hs
+++ b/test/Unit/Tricorder/SocketSpec.hs
@@ -1,6 +1,6 @@
 module Unit.Tricorder.SocketSpec (test_Socket) where
 
-import Atelier.Effects.File (File, runFile)
+import Atelier.Effects.File (File)
 import Atelier.Effects.FileSystem (FileSystem)
 import Effectful (IOE, runEff)
 import Effectful.Exception (trySync)
@@ -8,6 +8,7 @@
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (testCase, (@?=))
 
+import Atelier.Effects.File qualified as File
 import Atelier.Effects.FileSystem qualified as FileSystem
 
 import Tricorder.Socket.Client (isDaemonReady)
@@ -75,9 +76,9 @@
 
 -- | Run scripted socket operations (no Delay needed).
 runScripted :: [SocketScript] -> Eff '[UnixSocket, File, IOE] a -> IO a
-runScripted script = runEff . runFile . UnixSocket.runScripted script
+runScripted script = runEff . File.run . UnixSocket.runScripted script
 
 
 -- | Run socket operations against the real IO interpreter.
 runIO' :: Eff '[UnixSocket, File, FileSystem, IOE] a -> IO a
-runIO' = runEff . FileSystem.runFileSystemIO . runFile . UnixSocket.runIO
+runIO' = runEff . FileSystem.runIO . File.run . UnixSocket.runIO
diff --git a/test/Unit/Tricorder/SourceLookupSpec.hs b/test/Unit/Tricorder/SourceLookupSpec.hs
--- a/test/Unit/Tricorder/SourceLookupSpec.hs
+++ b/test/Unit/Tricorder/SourceLookupSpec.hs
@@ -1,9 +1,10 @@
 module Unit.Tricorder.SourceLookupSpec (test_SourceLookup) where
 
-import Atelier.Effects.Env (Env, runEnvConst)
+import Atelier.Effects.Env (Env)
 import Atelier.Effects.FileSystem (FileSystem (..))
 import Atelier.Effects.Input (Input)
-import Atelier.Effects.Log (Log, runLogNoOp)
+import Atelier.Effects.Log (Log)
+import Data.Default (def)
 import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe)
 import Effectful (IOE, runEff)
 import Effectful.Concurrent (Concurrent, runConcurrent)
@@ -16,7 +17,9 @@
 import Tricorder.SourceLookup.ReexportLimits (ReexportLimits (..), defaultReexportLimits)
 import Tricorder.SourceLookup.SourceQuery (ModuleName, SourceQuery (..))
 
+import Atelier.Effects.Env qualified as Env
 import Atelier.Effects.Input qualified as Input
+import Atelier.Effects.Log qualified as Log
 import Codec.Archive.Tar qualified as Tar
 import Codec.Archive.Tar.Entry qualified as Tar
 import Codec.Compression.GZip qualified as GZip
@@ -26,7 +29,6 @@
 import Data.Text qualified as T
 
 import Tricorder.Session.CabalFile (CabalFile (..))
-import Tricorder.Session.Repl (Repl (..))
 import Tricorder.SourceLookup
     ( ProjectSourceDirs (..)
     , lookupModuleSource
@@ -36,6 +38,7 @@
 import Tricorder.SourceLookup.Hackage (Hackage (..), Result (..))
 import Tricorder.SourceLookup.PackageStore (PackageStore)
 
+import Tricorder.Session.Stage.Build.Session qualified as Build
 import Tricorder.SourceLookup.PackageStore qualified as PackageStore
 
 
@@ -399,7 +402,7 @@
      , Env
      , FileSystem
      , State (Map FilePath LByteString)
-     , Input Repl
+     , Input Build.Session
      , Log
      , Concurrent
      , IOE
@@ -418,11 +421,11 @@
 runTest pkgScript initialFs onFetch action =
     runEff
         . runConcurrent
-        . runLogNoOp
-        . Input.withConst Cabal
+        . Log.runNoOp
+        . Input.withConst (def :: Build.Session)
         . evalState initialFs
         . runFileSystemFake
-        . runEnvConst [("HOME", "/h")]
+        . Env.runConst [("HOME", "/h")]
         . PackageStore.run
         . runHackageWith onFetch
         . runGhcPkgScripted pkgScript
diff --git a/test/Unit/Tricorder/WaitersSpec.hs b/test/Unit/Tricorder/WaitersSpec.hs
--- a/test/Unit/Tricorder/WaitersSpec.hs
+++ b/test/Unit/Tricorder/WaitersSpec.hs
@@ -1,6 +1,6 @@
 module Unit.Tricorder.WaitersSpec (test_Waiters) where
 
-import Atelier.Effects.Conc (Conc, runConc)
+import Atelier.Effects.Conc (Conc)
 import Control.Exception (try)
 import Effectful (IOE, runEff)
 import Effectful.Concurrent (Concurrent, runConcurrent)
@@ -30,7 +30,7 @@
 runWaitersTest =
     runEff
         . runConcurrent
-        . runConc
+        . Conc.runIO
         . runState @[Text] []
         . Waiters.run
 
diff --git a/tricorder.cabal b/tricorder.cabal
--- a/tricorder.cabal
+++ b/tricorder.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:            tricorder
-version:         0.7.0.0
+version:         1.0.0.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
@@ -125,7 +125,6 @@
       Tricorder.Session.State.Result
       Tricorder.Session.Target
       Tricorder.Session.TestTarget
-      Tricorder.Session.TestTimeout
       Tricorder.Session.Util
       Tricorder.Session.WatchDirs
       Tricorder.Session.WatchExclusionPatterns
@@ -172,7 +171,7 @@
     , Cabal-syntax >=3.12 && <3.17
     , Glob ==0.10.*
     , aeson >=2.2 && <2.4
-    , atelier-core ==0.8.*
+    , atelier-core ==1.0.*
     , atelier-prelude ==0.4.*
     , base >=4.18 && <4.23
     , brick ==3.0.*
@@ -201,7 +200,7 @@
     , text ==2.1.*
     , time >=1.12 && <1.17
     , time-units ==1.0.*
-    , tricorder-types ==0.6.*
+    , tricorder-types ==1.0.*
     , vty >=6.5 && <6.7
     , vty-crossplatform ==0.5.*
     , yaml ==0.11.*
@@ -354,7 +353,7 @@
   build-depends:
       Cabal-syntax >=3.12 && <3.17
     , aeson >=2.2 && <2.4
-    , atelier-core ==0.8.*
+    , atelier-core ==1.0.*
     , atelier-prelude ==0.4.*
     , base >=4.18 && <4.23
     , bytestring >=0.11 && <0.13
@@ -376,7 +375,7 @@
     , time >=1.12 && <1.17
     , time-units ==1.0.*
     , tricorder-internal
-    , tricorder-types ==0.6.*
+    , tricorder-types ==1.0.*
     , typed-process ==0.2.*
     , unagi-chan ==0.4.*
     , unix ==2.8.*
