diff --git a/components/amazonka/Effectful/Zoo/Amazonka/Api.hs b/components/amazonka/Effectful/Zoo/Amazonka/Api.hs
--- a/components/amazonka/Effectful/Zoo/Amazonka/Api.hs
+++ b/components/amazonka/Effectful/Zoo/Amazonka/Api.hs
@@ -3,7 +3,9 @@
 module Effectful.Zoo.Amazonka.Api
   ( module Effectful.Zoo.Amazonka.Api.Discover,
     module Effectful.Zoo.Amazonka.Api.Log,
+    module Effectful.Zoo.Amazonka.Api.Send,
   ) where
 
 import Effectful.Zoo.Amazonka.Api.Discover
 import Effectful.Zoo.Amazonka.Api.Log
+import Effectful.Zoo.Amazonka.Api.Send
diff --git a/components/amazonka/Effectful/Zoo/Amazonka/Api/Run.hs b/components/amazonka/Effectful/Zoo/Amazonka/Api/Run.hs
--- a/components/amazonka/Effectful/Zoo/Amazonka/Api/Run.hs
+++ b/components/amazonka/Effectful/Zoo/Amazonka/Api/Run.hs
@@ -4,14 +4,19 @@
   ( runDataLogAwsLogEntryToLog,
     runDataLogAwsLogEntryToLogWith,
     runDataLogAwsLogEntryLocalToLogWith,
+    runReaderAwsEnvDiscover,
   ) where
 
+import Amazonka qualified as AWS
 import Control.Monad.IO.Class
 import Data.ByteString.Builder qualified as B
+import Data.Generics.Product.Any
 import Data.Text.Lazy qualified as LT
 import Data.Text.Lazy.Encoding qualified as LT
 import Data.Time.Clock qualified as IO
 import Effectful
+import Effectful.Environment
+import Effectful.Reader.Static
 import Effectful.Zoo.Amazonka.Api
 import Effectful.Zoo.Amazonka.Data
 import Effectful.Zoo.Core
@@ -21,6 +26,8 @@
 import Effectful.Zoo.Log.Data.LogMessage
 import Effectful.Zoo.Log.Data.Severity
 import HaskellWorks.Prelude
+import Lens.Micro
+import System.IO qualified as IO
 
 runDataLogAwsLogEntryToLog :: forall a r. ()
   => r <: DataLog (LogEntry (LogMessage Text))
@@ -61,3 +68,26 @@
         , time = now
         , source = entry.callStack
         }
+
+runReaderAwsEnvDiscover :: forall a r. ()
+  => r <: Environment
+  => r <: IOE
+  => Eff (Reader AwsEnv : r) a
+  -> Eff r a
+runReaderAwsEnvDiscover f = do
+  logger' <- liftIO $ AWS.newLogger AWS.Debug IO.stdout
+
+  mLocalStackHost <- lookupEnv "AWS_LOCALSTACK_HOST"
+  mLocalStackPort <- lookupEnv "AWS_LOCALSTACK_PORT"
+
+  mLocalStackEndpoint <- pure $ (,)
+    <$> mLocalStackHost
+    <*> mLocalStackPort
+
+  discoveredAwsEnv <- liftIO $ AWS.newEnv AWS.discover
+
+  awsEnv <- pure $ discoveredAwsEnv
+    & the @"logger" .~ logger'
+    & the @"overrides" %~ maybeSetEndpoint mLocalStackEndpoint
+
+  runReader awsEnv f
diff --git a/components/blockfrost/Effectful/Zoo/Blockfrost/Api.hs b/components/blockfrost/Effectful/Zoo/Blockfrost/Api.hs
--- a/components/blockfrost/Effectful/Zoo/Blockfrost/Api.hs
+++ b/components/blockfrost/Effectful/Zoo/Blockfrost/Api.hs
@@ -339,10 +339,10 @@
 import Data.Text
 import Effectful
 import Effectful.Dispatch.Dynamic
-import Effectful.Error.Static
 import Effectful.Zoo.Blockfrost.Dynamic (Blockfrost, runBlockfrost)
 import Effectful.Zoo.Blockfrost.Dynamic qualified as BF
 import Effectful.Zoo.Core
+import Effectful.Zoo.Error.Static
 import HaskellWorks.Prelude
 
 -- Client
@@ -507,15 +507,6 @@
 getTxMetadataByLabelJSON            :: r <: Blockfrost => r <: Error BlockfrostError => Text -> Eff r [TxMetaJSON]
 getTxMetadataByLabelCBOR'           :: r <: Blockfrost => r <: Error BlockfrostError => Text -> Paged -> SortOrder -> Eff r [TxMetaCBOR]
 getTxMetadataByLabelCBOR            :: r <: Blockfrost => r <: Error BlockfrostError => Text -> Eff r [TxMetaCBOR]
-
-
-fromEither :: forall e a r. ()
-  => Show e
-  => r <: Error e
-  => Either e a
-  -> Eff r a
-fromEither =
-  either throwError pure
 
 -- Client
 getRoot                                     = fromEither =<< send do BF.GetRoot
diff --git a/components/blockfrost/Effectful/Zoo/Blockfrost/Dynamic.hs b/components/blockfrost/Effectful/Zoo/Blockfrost/Dynamic.hs
--- a/components/blockfrost/Effectful/Zoo/Blockfrost/Dynamic.hs
+++ b/components/blockfrost/Effectful/Zoo/Blockfrost/Dynamic.hs
@@ -11,7 +11,7 @@
 import Data.Maybe
 import Effectful
 import Effectful.Dispatch.Dynamic
-import Effectful.Reader.Dynamic
+import Effectful.Reader.Static
 import Effectful.Zoo.Core
 import HaskellWorks.Prelude
 
diff --git a/components/core/Effectful/Zoo/Aeson.hs b/components/core/Effectful/Zoo/Aeson.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Aeson.hs
@@ -0,0 +1,21 @@
+module Effectful.Zoo.Aeson
+  ( aesonDecode,
+  ) where
+
+import Data.Aeson (FromJSON)
+import Data.Aeson qualified as Aeson
+import Data.ByteString.Lazy qualified as LBS
+import Effectful
+import Effectful.Zoo.Core
+import Effectful.Zoo.Error.Static
+import HaskellWorks.Error.Types.JsonDecodeError
+import HaskellWorks.Prelude
+
+aesonDecode :: forall a r. ()
+  => r <: Error JsonDecodeError
+  => FromJSON a
+  => LBS.ByteString
+  -> Eff r a
+aesonDecode bs =
+  fromEither (Aeson.eitherDecode bs)
+    & mapError newJsonDecodeError
diff --git a/components/core/Effectful/Zoo/Core.hs b/components/core/Effectful/Zoo/Core.hs
--- a/components/core/Effectful/Zoo/Core.hs
+++ b/components/core/Effectful/Zoo/Core.hs
@@ -1,5 +1,6 @@
 module Effectful.Zoo.Core
-  ( type (<:)
+  ( type (<:),
+    type (<<:),
   ) where
 
-import           Effectful.Zoo.Core.Prim
+import Effectful.Zoo.Prim
diff --git a/components/core/Effectful/Zoo/Core/Error/Dynamic.hs b/components/core/Effectful/Zoo/Core/Error/Dynamic.hs
deleted file mode 100644
--- a/components/core/Effectful/Zoo/Core/Error/Dynamic.hs
+++ /dev/null
@@ -1,100 +0,0 @@
-module Effectful.Zoo.Core.Error.Dynamic
-  ( Error,
-    catch,
-    catch_,
-    throw,
-    trap,
-    trap_,
-
-    catchWithCallStack,
-    catchWithCallStack_,
-    trapWithCallStack,
-    trapWithCallStack_,
-  ) where
-
-import Effectful
-import Effectful.Error.Dynamic (Error)
-import Effectful.Error.Dynamic qualified as E
-import Effectful.Zoo.Core
-import HaskellWorks.Prelude
-
-catchWithCallStack :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> (CallStack -> e -> Eff es a)
-  -> Eff es a
-catchWithCallStack =
-  E.catchError
-
-catchWithCallStack_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> (CallStack -> Eff es a)
-  -> Eff es a
-catchWithCallStack_ f h =
-  catchWithCallStack @e f \cs _ -> h cs
-
-trapWithCallStack :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => (CallStack -> e -> Eff es a)
-  -> Eff es a
-  -> Eff es a
-trapWithCallStack =
-  flip catchWithCallStack
-
-trapWithCallStack_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => (CallStack -> Eff es a)
-  -> Eff es a
-  -> Eff es a
-trapWithCallStack_ h =
-  trapWithCallStack @e (const . h)
-
-catch :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> (e -> Eff es a)
-  -> Eff es a
-catch action handler =
-  catchWithCallStack action (const handler)
-
-catch_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> Eff es a
-  -> Eff es a
-catch_ action handler =
-  catch @e action (const handler)
-
-throw :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Show e
-  => e
-  -> Eff es a
-throw =
-  E.throwError
-
-trap :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => (e -> Eff es a)
-  -> Eff es a
-  -> Eff es a
-trap =
-  flip catch
-
-trap_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> Eff es a
-  -> Eff es a
-trap_ handler =
-  trap @e (const handler)
diff --git a/components/core/Effectful/Zoo/Core/Error/Static.hs b/components/core/Effectful/Zoo/Core/Error/Static.hs
deleted file mode 100644
--- a/components/core/Effectful/Zoo/Core/Error/Static.hs
+++ /dev/null
@@ -1,175 +0,0 @@
-module Effectful.Zoo.Core.Error.Static
-  ( Error,
-
-    throw,
-
-    catch,
-    catch_,
-    trap,
-    trap_,
-
-    catchWithCallStack,
-    catchWithCallStack_,
-    trapWithCallStack,
-    trapWithCallStack_,
-
-    catchIn,
-    catchIn_,
-    trapIn,
-    trapIn_,
-
-    catchWithCallStackIn,
-    catchWithCallStackIn_,
-    trapWithCallStackIn,
-    trapWithCallStackIn_,
-  ) where
-
-import Effectful
-import Effectful.Error.Static (Error)
-import Effectful.Error.Static qualified as E
-import Effectful.Zoo.Core
-import HaskellWorks.Prelude
-
-throw :: forall e r a. ()
-  => HasCallStack
-  => r <: Error e
-  => Show e
-  => e
-  -> Eff r a
-throw =
-  E.throwError
-
-catchWithCallStack :: forall e r a. ()
-  => HasCallStack
-  => Eff (Error e : r) a
-  -> (CallStack -> e -> Eff r a)
-  -> Eff r a
-catchWithCallStack action handler =
-  E.runError action
-    & onLeftM (uncurry handler)
-
-catchWithCallStack_ :: forall e r a. ()
-  => HasCallStack
-  => Eff (Error e : r) a
-  -> (CallStack -> Eff r a)
-  -> Eff r a
-catchWithCallStack_ f h =
-  catchWithCallStack @e f \cs _ -> h cs
-
-trapWithCallStack :: forall e r a. ()
-  => HasCallStack
-  => (CallStack -> e -> Eff r a)
-  -> Eff (Error e : r) a
-  -> Eff r a
-trapWithCallStack =
-  flip catchWithCallStack
-
-trapWithCallStack_ :: forall e r a. ()
-  => HasCallStack
-  => (CallStack -> Eff r a)
-  -> Eff (Error e : r) a
-  -> Eff r a
-trapWithCallStack_ h =
-  trapWithCallStack @e (const . h)
-
-catch :: forall e r a. ()
-  => Eff (Error e : r) a
-  -> (e -> Eff r a)
-  -> Eff r a
-catch action handler =
-  E.runError action
-    & onLeftM (handler . snd)
-
-catch_ :: forall e r a. ()
-  => Eff (Error e : r) a
-  -> Eff r a
-  -> Eff r a
-catch_ action handler =
-  catch @e action (const handler)
-
-trap :: forall e r a. ()
-  => (e -> Eff r a)
-  -> Eff (Error e : r) a
-  -> Eff r a
-trap =
-  flip catch
-
-trap_ :: forall e r a. ()
-  => Eff r a
-  -> Eff (Error e : r) a
-  -> Eff r a
-trap_ handler =
-  trap @e (const handler)
-
-
-catchWithCallStackIn :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> (CallStack -> e -> Eff es a)
-  -> Eff es a
-catchWithCallStackIn =
-  E.catchError
-
-catchWithCallStackIn_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> (CallStack -> Eff es a)
-  -> Eff es a
-catchWithCallStackIn_ f h =
-  catchWithCallStackIn @e f \cs _ -> h cs
-
-trapWithCallStackIn :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => (CallStack -> e -> Eff es a)
-  -> Eff es a
-  -> Eff es a
-trapWithCallStackIn =
-  flip catchWithCallStackIn
-
-trapWithCallStackIn_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => (CallStack -> Eff es a)
-  -> Eff es a
-  -> Eff es a
-trapWithCallStackIn_ h =
-  trapWithCallStackIn @e (const . h)
-
-catchIn :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> (e -> Eff es a)
-  -> Eff es a
-catchIn action handler =
-  catchWithCallStackIn action (const handler)
-
-catchIn_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> Eff es a
-  -> Eff es a
-catchIn_ action handler =
-  catchIn @e action (const handler)
-
-trapIn :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => (e -> Eff es a)
-  -> Eff es a
-  -> Eff es a
-trapIn =
-  flip catchIn
-
-trapIn_ :: forall e es a. ()
-  => HasCallStack
-  => es <: Error e
-  => Eff es a
-  -> Eff es a
-  -> Eff es a
-trapIn_ handler =
-  trapIn @e (const handler)
diff --git a/components/core/Effectful/Zoo/Core/Exception.hs b/components/core/Effectful/Zoo/Core/Exception.hs
deleted file mode 100644
--- a/components/core/Effectful/Zoo/Core/Exception.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-module Effectful.Zoo.Core.Exception
-  ( Exception,
-    catchIO,
-    throwIO,
-    trapIO,
-  ) where
-
-import Effectful
-import Effectful.Exception (Exception)
-import Effectful.Exception qualified as E
-import HaskellWorks.Prelude
-
-catchIO :: ()
-  => E.Exception e
-  => Eff es a
-  -> (e -> Eff es a)
-  -> Eff es a
-catchIO =
-  E.catch
-
-throwIO :: ()
-  => HasCallStack
-  => E.Exception e
-  => e
-  -> Eff es a
-throwIO =
-  E.throwIO
-
-trapIO
-  :: E.Exception e
-  => (e -> Eff es a)
-  -> Eff es a
-  -> Eff es a
-trapIO =
-  flip catchIO
diff --git a/components/core/Effectful/Zoo/Core/Function.hs b/components/core/Effectful/Zoo/Core/Function.hs
deleted file mode 100644
--- a/components/core/Effectful/Zoo/Core/Function.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Effectful.Zoo.Core.Function
-  ( once,
-  ) where
-
-import Effectful
-import Effectful.Concurrent
-import Effectful.Concurrent.MVar
-import Effectful.Zoo.Core.Prim
-import HaskellWorks.Prelude
-
-once :: ()
-  => r <: Concurrent
-  => Eff r a
-  -> Eff r (Eff r a)
-once f = do
-  cache <- newMVar Nothing
-  return $ do
-    resultOrRunning <- takeMVar cache
-    case resultOrRunning of
-      Just result -> do
-        putMVar cache (Just result)
-        return result
-      Nothing -> do
-        result <- f
-        putMVar cache (Just result)
-        return result
diff --git a/components/core/Effectful/Zoo/Core/Prim.hs b/components/core/Effectful/Zoo/Core/Prim.hs
deleted file mode 100644
--- a/components/core/Effectful/Zoo/Core/Prim.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Effectful.Zoo.Core.Prim
-  ( type (<:)
-  ) where
-
-import           Effectful ((:>))
-
-type (<:) r e = (:>) e r
diff --git a/components/core/Effectful/Zoo/DataLog/Api.hs b/components/core/Effectful/Zoo/DataLog/Api.hs
--- a/components/core/Effectful/Zoo/DataLog/Api.hs
+++ b/components/core/Effectful/Zoo/DataLog/Api.hs
@@ -1,14 +1,25 @@
 module Effectful.Zoo.DataLog.Api
   ( dataLog,
+    logEntryToJson,
+    logMessageToJson,
+    putJsonStdout,
   ) where
 
+import Data.Aeson (Value, object, (.=))
+import Data.Aeson qualified as J
+import Data.ByteString.Lazy qualified as LBS
 import Effectful
 import Effectful.Dispatch.Dynamic
+import Effectful.Dispatch.Static
 import Effectful.Zoo.Core
+import Effectful.Zoo.DataLog.Data.LogEntry
 import Effectful.Zoo.DataLog.Dynamic
+import Effectful.Zoo.Log.Data.LogMessage
+import GHC.Stack qualified as GHC
 import HaskellWorks.Prelude
+import System.IO qualified as IO
 
-dataLog :: ()
+dataLog :: forall i r. ()
   => HasCallStack
   => r <: DataLog i
   => i
@@ -16,3 +27,41 @@
 dataLog i =
   withFrozenCallStack do
     send $ DataLog i
+
+logEntryToJson :: forall a. ()
+  => (a -> Value)
+  -> LogEntry a
+  -> Value
+logEntryToJson aToJson (LogEntry value time callstack) =
+    object
+      [ "time" .= time
+      , "data" .= aToJson value
+      , "callstack" .= fmap callsiteToJson (GHC.getCallStack callstack)
+      ]
+    where
+      callsiteToJson :: ([Char], GHC.SrcLoc) -> Value
+      callsiteToJson (caller, srcLoc) =
+        object
+          [ "caller"    .= caller
+          , "package"   .= GHC.srcLocPackage srcLoc
+          , "module"    .= GHC.srcLocModule srcLoc
+          , "file"      .= GHC.srcLocFile srcLoc
+          , "startLine" .= GHC.srcLocStartLine srcLoc
+          , "startCol"  .= GHC.srcLocStartCol srcLoc
+          , "endLine"   .= GHC.srcLocEndLine srcLoc
+          , "endCol"    .= GHC.srcLocEndCol srcLoc
+          ]
+
+logMessageToJson :: LogMessage Text -> Value
+logMessageToJson (LogMessage severity message) =
+    object
+      [ "severity" .= show severity
+      , "message"  .= message
+      ]
+
+putJsonStdout :: ()
+  => Value
+  -> Eff r ()
+putJsonStdout value = do
+  unsafeEff_ $ LBS.putStr $ J.encode value <> "\n"
+  unsafeEff_ $ IO.hFlush IO.stdout
diff --git a/components/core/Effectful/Zoo/Environment.hs b/components/core/Effectful/Zoo/Environment.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Environment.hs
@@ -0,0 +1,88 @@
+module Effectful.Zoo.Environment
+  ( -- * Effect
+    Environment,
+
+    -- ** Handlers
+    E.runEnvironment,
+
+    -- * Querying the environment
+    E.getArgs,
+    E.getProgName,
+    E.getExecutablePath,
+    E.getEnv,
+    E.getEnvironment,
+    lookupEnv,
+    lookupEnvMaybe,
+    lookupMapEnv,
+    lookupParseMaybeEnv,
+    lookupParseEitherEnv,
+
+    -- * Modifying the environment
+    E.setEnv,
+    E.unsetEnv,
+    E.withArgs,
+    E.withProgName,
+  )
+  where
+
+import Data.Text qualified as T
+import Effectful
+import Effectful.Environment (Environment)
+import Effectful.Environment qualified as E
+import Effectful.Zoo.Core
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Errors.EnvironmentVariableInvalid
+import Effectful.Zoo.Errors.EnvironmentVariableMissing
+import HaskellWorks.Prelude
+
+lookupEnv :: ()
+  => r <: Environment
+  => r <: Error EnvironmentVariableMissing
+  => Text
+  -> Eff r Text
+lookupEnv envName =
+  lookupEnvMaybe envName
+    & onNothingM (throw $ EnvironmentVariableMissing envName)
+
+lookupMapEnv :: ()
+  => r <: Environment
+  => r <: Error EnvironmentVariableMissing
+  => Text
+  -> (Text -> a)
+  -> Eff r a
+lookupMapEnv envName f =
+  f <$> lookupEnv envName
+
+lookupParseMaybeEnv :: ()
+  => r <: Environment
+  => r <: Error EnvironmentVariableInvalid
+  => r <: Error EnvironmentVariableMissing
+  => Text
+  -> (Text -> Maybe a)
+  -> Eff r a
+lookupParseMaybeEnv envName parse = do
+  text <- lookupEnv envName
+
+  parse text
+    & onNothing (throw $ EnvironmentVariableInvalid envName text Nothing)
+
+lookupParseEitherEnv :: ()
+  => r <: Environment
+  => r <: Error EnvironmentVariableInvalid
+  => r <: Error EnvironmentVariableMissing
+  => Text
+  -> (Text -> Either Text a)
+  -> Eff r a
+lookupParseEitherEnv envName parse = do
+  text <- lookupEnv envName
+
+  parse text
+    & onLeft (throw . EnvironmentVariableInvalid envName text . Just)
+
+lookupEnvMaybe :: ()
+  => r <: Environment
+  => Text
+  -> Eff r (Maybe Text)
+lookupEnvMaybe envName = do
+  value <- E.lookupEnv $ T.unpack envName
+  pure $ T.pack <$> value
diff --git a/components/core/Effectful/Zoo/Error/Dynamic.hs b/components/core/Effectful/Zoo/Error/Dynamic.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Error/Dynamic.hs
@@ -0,0 +1,212 @@
+module Effectful.Zoo.Error.Dynamic
+  ( Error,
+
+    throw,
+
+    catch,
+    catch_,
+    trap,
+    trap_,
+
+    catchWithCallStack,
+    catchWithCallStack_,
+    trapWithCallStack,
+    trapWithCallStack_,
+
+    catchIn,
+    catchIn_,
+    trapIn,
+    trapIn_,
+
+    catchWithCallStackIn,
+    catchWithCallStackIn_,
+    trapWithCallStackIn,
+    trapWithCallStackIn_,
+
+    fromEither,
+    mapError,
+    runError,
+    runError_,
+    runErrorMap_,
+  ) where
+
+import Effectful
+import Effectful.Error.Dynamic (Error, runError)
+import Effectful.Error.Dynamic qualified as E
+import Effectful.Zoo.Core
+import HaskellWorks.Prelude
+
+throw :: forall e r a. ()
+  => HasCallStack
+  => r <: Error e
+  => Show e
+  => e
+  -> Eff r a
+throw =
+  E.throwError
+
+catchWithCallStack :: forall e r a. ()
+  => HasCallStack
+  => Eff (Error e : r) a
+  -> (CallStack -> e -> Eff r a)
+  -> Eff r a
+catchWithCallStack action handler =
+  E.runError action
+    & onLeftM (uncurry handler)
+
+catchWithCallStack_ :: forall e r a. ()
+  => HasCallStack
+  => Eff (Error e : r) a
+  -> (CallStack -> Eff r a)
+  -> Eff r a
+catchWithCallStack_ f h =
+  catchWithCallStack @e f \cs _ -> h cs
+
+trapWithCallStack :: forall e r a. ()
+  => HasCallStack
+  => (CallStack -> e -> Eff r a)
+  -> Eff (Error e : r) a
+  -> Eff r a
+trapWithCallStack =
+  flip catchWithCallStack
+
+trapWithCallStack_ :: forall e r a. ()
+  => HasCallStack
+  => (CallStack -> Eff r a)
+  -> Eff (Error e : r) a
+  -> Eff r a
+trapWithCallStack_ h =
+  trapWithCallStack @e (const . h)
+
+catch :: forall e r a. ()
+  => Eff (Error e : r) a
+  -> (e -> Eff r a)
+  -> Eff r a
+catch action handler =
+  E.runError action
+    & onLeftM (handler . snd)
+
+catch_ :: forall e r a. ()
+  => Eff (Error e : r) a
+  -> Eff r a
+  -> Eff r a
+catch_ action handler =
+  catch @e action (const handler)
+
+trap :: forall e r a. ()
+  => (e -> Eff r a)
+  -> Eff (Error e : r) a
+  -> Eff r a
+trap =
+  flip catch
+
+trap_ :: forall e r a. ()
+  => Eff r a
+  -> Eff (Error e : r) a
+  -> Eff r a
+trap_ handler =
+  trap @e (const handler)
+
+catchWithCallStackIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> (CallStack -> e -> Eff es a)
+  -> Eff es a
+catchWithCallStackIn =
+  E.catchError
+
+catchWithCallStackIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> (CallStack -> Eff es a)
+  -> Eff es a
+catchWithCallStackIn_ f h =
+  catchWithCallStackIn @e f \cs _ -> h cs
+
+trapWithCallStackIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => (CallStack -> e -> Eff es a)
+  -> Eff es a
+  -> Eff es a
+trapWithCallStackIn =
+  flip catchWithCallStackIn
+
+trapWithCallStackIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => (CallStack -> Eff es a)
+  -> Eff es a
+  -> Eff es a
+trapWithCallStackIn_ h =
+  trapWithCallStackIn @e (const . h)
+
+catchIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> (e -> Eff es a)
+  -> Eff es a
+catchIn action handler =
+  catchWithCallStackIn action (const handler)
+
+catchIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> Eff es a
+  -> Eff es a
+catchIn_ action handler =
+  catchIn @e action (const handler)
+
+trapIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => (e -> Eff es a)
+  -> Eff es a
+  -> Eff es a
+trapIn =
+  flip catchIn
+
+trapIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> Eff es a
+  -> Eff es a
+trapIn_ handler =
+  trapIn @e (const handler)
+
+fromEither :: forall e a r. ()
+  => Show e
+  => r <: Error e
+  => Either e a
+  -> Eff r a
+fromEither =
+  either throw pure
+
+mapError :: forall d e a r. ()
+  => HasCallStack
+  => r <: Error e
+  => Show e
+  => (d -> e)
+  -> Eff (Error d : r) a
+  -> Eff r a
+mapError f g =
+  g & trap (throw . f)
+
+runError_ :: ()
+  => Eff (Error e : r) a
+  -> Eff r (Either e a)
+runError_ =
+  fmap (first snd) . runError
+
+-- | Run an 'Error' effect and map the error value to a different type.
+runErrorMap_ :: ()
+  => (e -> d)
+  -> Eff (Error e : r) a
+  -> Eff r (Either d a)
+runErrorMap_ f =
+  fmap (first (f . snd)) . runError
diff --git a/components/core/Effectful/Zoo/Error/Static.hs b/components/core/Effectful/Zoo/Error/Static.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Error/Static.hs
@@ -0,0 +1,212 @@
+module Effectful.Zoo.Error.Static
+  ( Error,
+
+    throw,
+
+    catch,
+    catch_,
+    trap,
+    trap_,
+
+    catchWithCallStack,
+    catchWithCallStack_,
+    trapWithCallStack,
+    trapWithCallStack_,
+
+    catchIn,
+    catchIn_,
+    trapIn,
+    trapIn_,
+
+    catchWithCallStackIn,
+    catchWithCallStackIn_,
+    trapWithCallStackIn,
+    trapWithCallStackIn_,
+
+    fromEither,
+    mapError,
+    runError,
+    runError_,
+    runErrorMap_,
+  ) where
+
+import Effectful
+import Effectful.Error.Static (Error, runError)
+import Effectful.Error.Static qualified as E
+import Effectful.Zoo.Prim
+import HaskellWorks.Prelude
+
+throw :: forall e r a. ()
+  => HasCallStack
+  => r <: Error e
+  => Show e
+  => e
+  -> Eff r a
+throw =
+  E.throwError
+
+catchWithCallStack :: forall e r a. ()
+  => HasCallStack
+  => Eff (Error e : r) a
+  -> (CallStack -> e -> Eff r a)
+  -> Eff r a
+catchWithCallStack action handler =
+  E.runError action
+    & onLeftM (uncurry handler)
+
+catchWithCallStack_ :: forall e r a. ()
+  => HasCallStack
+  => Eff (Error e : r) a
+  -> (CallStack -> Eff r a)
+  -> Eff r a
+catchWithCallStack_ f h =
+  catchWithCallStack @e f \cs _ -> h cs
+
+trapWithCallStack :: forall e r a. ()
+  => HasCallStack
+  => (CallStack -> e -> Eff r a)
+  -> Eff (Error e : r) a
+  -> Eff r a
+trapWithCallStack =
+  flip catchWithCallStack
+
+trapWithCallStack_ :: forall e r a. ()
+  => HasCallStack
+  => (CallStack -> Eff r a)
+  -> Eff (Error e : r) a
+  -> Eff r a
+trapWithCallStack_ h =
+  trapWithCallStack @e (const . h)
+
+catch :: forall e r a. ()
+  => Eff (Error e : r) a
+  -> (e -> Eff r a)
+  -> Eff r a
+catch action handler =
+  E.runError action
+    & onLeftM (handler . snd)
+
+catch_ :: forall e r a. ()
+  => Eff (Error e : r) a
+  -> Eff r a
+  -> Eff r a
+catch_ action handler =
+  catch @e action (const handler)
+
+trap :: forall e r a. ()
+  => (e -> Eff r a)
+  -> Eff (Error e : r) a
+  -> Eff r a
+trap =
+  flip catch
+
+trap_ :: forall e r a. ()
+  => Eff r a
+  -> Eff (Error e : r) a
+  -> Eff r a
+trap_ handler =
+  trap @e (const handler)
+
+catchWithCallStackIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> (CallStack -> e -> Eff es a)
+  -> Eff es a
+catchWithCallStackIn =
+  E.catchError
+
+catchWithCallStackIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> (CallStack -> Eff es a)
+  -> Eff es a
+catchWithCallStackIn_ f h =
+  catchWithCallStackIn @e f \cs _ -> h cs
+
+trapWithCallStackIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => (CallStack -> e -> Eff es a)
+  -> Eff es a
+  -> Eff es a
+trapWithCallStackIn =
+  flip catchWithCallStackIn
+
+trapWithCallStackIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => (CallStack -> Eff es a)
+  -> Eff es a
+  -> Eff es a
+trapWithCallStackIn_ h =
+  trapWithCallStackIn @e (const . h)
+
+catchIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> (e -> Eff es a)
+  -> Eff es a
+catchIn action handler =
+  catchWithCallStackIn action (const handler)
+
+catchIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> Eff es a
+  -> Eff es a
+catchIn_ action handler =
+  catchIn @e action (const handler)
+
+trapIn :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => (e -> Eff es a)
+  -> Eff es a
+  -> Eff es a
+trapIn =
+  flip catchIn
+
+trapIn_ :: forall e es a. ()
+  => HasCallStack
+  => es <: Error e
+  => Eff es a
+  -> Eff es a
+  -> Eff es a
+trapIn_ handler =
+  trapIn @e (const handler)
+
+fromEither :: forall e a r. ()
+  => Show e
+  => r <: Error e
+  => Either e a
+  -> Eff r a
+fromEither =
+  either throw pure
+
+mapError :: forall d e a r. ()
+  => HasCallStack
+  => r <: Error e
+  => Show e
+  => (d -> e)
+  -> Eff (Error d : r) a
+  -> Eff r a
+mapError f g =
+  g & trap (throw . f)
+
+runError_ :: ()
+  => Eff (Error e : r) a
+  -> Eff r (Either e a)
+runError_ =
+  fmap (first snd) . runError
+
+-- | Run an 'Error' effect and map the error value to a different type.
+runErrorMap_ :: ()
+  => (e -> d)
+  -> Eff (Error e : r) a
+  -> Eff r (Either d a)
+runErrorMap_ f =
+  fmap (first (f . snd)) . runError
diff --git a/components/core/Effectful/Zoo/Errors/EnvironmentVariableInvalid.hs b/components/core/Effectful/Zoo/Errors/EnvironmentVariableInvalid.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Errors/EnvironmentVariableInvalid.hs
@@ -0,0 +1,24 @@
+module Effectful.Zoo.Errors.EnvironmentVariableInvalid
+  ( EnvironmentVariableInvalid (..),
+  ) where
+
+import Data.Aeson (ToJSON (..), (.=))
+import Data.Aeson qualified as J
+import GHC.Generics
+import HaskellWorks.Prelude
+
+data EnvironmentVariableInvalid = EnvironmentVariableInvalid
+    { variable :: Text
+    , text :: Text
+    , reason :: Maybe Text
+    }
+    deriving stock (Eq, Show, Generic)
+
+instance ToJSON EnvironmentVariableInvalid where
+    toJSON e =
+        J.object
+            [ "error" .= id @Text "EnvironmentVariableInvalid"
+            , "variable" .= e.variable
+            , "text" .= e.text
+            , "reason" .= e.reason
+            ]
diff --git a/components/core/Effectful/Zoo/Errors/EnvironmentVariableMissing.hs b/components/core/Effectful/Zoo/Errors/EnvironmentVariableMissing.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Errors/EnvironmentVariableMissing.hs
@@ -0,0 +1,20 @@
+module Effectful.Zoo.Errors.EnvironmentVariableMissing
+  ( EnvironmentVariableMissing (..),
+  ) where
+
+import Data.Aeson (ToJSON (..), (.=))
+import Data.Aeson qualified as J
+import GHC.Generics
+import HaskellWorks.Prelude
+
+newtype EnvironmentVariableMissing = EnvironmentVariableMissing
+    { variable :: Text
+    }
+    deriving stock (Eq, Show, Generic)
+
+instance ToJSON EnvironmentVariableMissing where
+    toJSON e =
+        J.object
+            [ "error" .= id @Text "EnvironmentVariableMissing"
+            , "variable" .= e.variable
+            ]
diff --git a/components/core/Effectful/Zoo/Exception.hs b/components/core/Effectful/Zoo/Exception.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Exception.hs
@@ -0,0 +1,35 @@
+module Effectful.Zoo.Exception
+  ( Exception,
+    catchIO,
+    throwIO,
+    trapIO,
+  ) where
+
+import Effectful
+import Effectful.Exception (Exception)
+import Effectful.Exception qualified as E
+import HaskellWorks.Prelude
+
+catchIO :: ()
+  => E.Exception e
+  => Eff es a
+  -> (e -> Eff es a)
+  -> Eff es a
+catchIO =
+  E.catch
+
+throwIO :: ()
+  => HasCallStack
+  => E.Exception e
+  => e
+  -> Eff es a
+throwIO =
+  E.throwIO
+
+trapIO
+  :: E.Exception e
+  => (e -> Eff es a)
+  -> Eff es a
+  -> Eff es a
+trapIO =
+  flip catchIO
diff --git a/components/core/Effectful/Zoo/FileSystem.hs b/components/core/Effectful/Zoo/FileSystem.hs
--- a/components/core/Effectful/Zoo/FileSystem.hs
+++ b/components/core/Effectful/Zoo/FileSystem.hs
@@ -4,8 +4,20 @@
     YamlDecodeError(..),
     readByteStringFile,
     readLazyByteStringFile,
+    readStringFile,
     readJsonFile,
     readYamlFile,
+    writeStringFile,
+    getCanonicalTemporaryDirectory,
+    createTempDirectory,
+    removePathForcibly,
+    doesFileExist,
+    doesDirectoryExist,
+
+    runFileSystem,
+
+    getCurrentDirectory,
+    canonicalizePath,
   ) where
 
 import Data.Aeson (FromJSON)
@@ -16,17 +28,32 @@
 import Data.Yaml qualified as Y
 import Effectful
 import Effectful.Dispatch.Static
-import Effectful.FileSystem
+import Effectful.FileSystem (FileSystem, runFileSystem)
 import Effectful.Zoo.Core
-import Effectful.Zoo.Core.Error.Static
-import Effectful.Zoo.Core.Exception
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Exception
 import Effectful.Zoo.Log.Api.Text
 import Effectful.Zoo.Log.Dynamic
 import Effectful.Zoo.Unsafe
 import HaskellWorks.Error.Types.JsonDecodeError
 import HaskellWorks.Error.Types.YamlDecodeError
 import HaskellWorks.Prelude
+import System.Directory qualified as D
+import System.IO qualified as IO
+import System.IO.Temp qualified as IO
 
+readStringFile :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Log Text
+  => FilePath
+  -> Eff r String
+readStringFile filePath = withFrozenCallStack do
+  info $ "Reading bytestring from file: " <> T.pack filePath
+  unsafeFileSystemEff_ (IO.readFile filePath)
+    & trapIO @IOException throw
+
 readLazyByteStringFile :: ()
   => HasCallStack
   => r <: Error IOException
@@ -34,7 +61,7 @@
   => r <: Log Text
   => FilePath
   -> Eff r LBS.ByteString
-readLazyByteStringFile filePath = withFrozenCallStack $ do
+readLazyByteStringFile filePath = withFrozenCallStack do
   info $ "Reading bytestring from file: " <> T.pack filePath
   unsafeFileSystemEff_ (LBS.readFile filePath)
     & trapIO @IOException throw
@@ -46,7 +73,7 @@
   => r <: Log Text
   => FilePath
   -> Eff r ByteString
-readByteStringFile filePath = withFrozenCallStack $ do
+readByteStringFile filePath = withFrozenCallStack do
   info $ "Reading bytestring from file: " <> T.pack filePath
   unsafeFileSystemEff_ (BS.readFile filePath)
     & trapIO @IOException throw
@@ -61,7 +88,7 @@
   => r <: Log Text
   => FilePath
   -> Eff r a
-readJsonFile filePath = withFrozenCallStack $ do
+readJsonFile filePath = withFrozenCallStack do
   info $ "Reading JSON file: " <> T.pack filePath
   contents <- readLazyByteStringFile filePath
   J.eitherDecode contents
@@ -77,8 +104,103 @@
   => r <: Log Text
   => FilePath
   -> Eff r a
-readYamlFile filePath = withFrozenCallStack $ do
+readYamlFile filePath = withFrozenCallStack do
   info $ "Reading YAML file: " <> T.pack filePath
   contents <- LBS.toStrict <$> readLazyByteStringFile filePath
   Y.decodeEither' contents
     & onLeft (throw . YamlDecodeError . T.pack . Y.prettyPrintParseException)
+
+writeStringFile :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Log Text
+  => FilePath
+  -> String
+  -> Eff r ()
+writeStringFile filePath contents = withFrozenCallStack do
+  info $ "Writing string to file: " <> T.pack filePath
+  unsafeFileSystemEff_ (IO.writeFile filePath contents)
+    & trapIO @IOException throw
+
+getCanonicalTemporaryDirectory :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => Eff r FilePath
+getCanonicalTemporaryDirectory = withFrozenCallStack do
+  unsafeFileSystemEff_ IO.getCanonicalTemporaryDirectory
+    & trapIO @IOException throw
+{-# INLINE getCanonicalTemporaryDirectory #-}
+
+createTempDirectory :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => FilePath
+  -> String
+  -> Eff r FilePath
+createTempDirectory fp template = withFrozenCallStack do
+  unsafeFileSystemEff_ (IO.createTempDirectory fp template)
+    & trapIO @IOException throw
+{-# INLINE createTempDirectory #-}
+
+removePathForcibly :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Log Text
+  => FilePath
+  -> Eff r ()
+removePathForcibly fp = withFrozenCallStack $ do
+  info $ "Calling: removePathForcibly " <> tshow fp
+
+  unsafeFileSystemEff_ (D.removePathForcibly fp)
+    & trapIO @IOException throw
+
+doesFileExist :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Log Text
+  => FilePath
+  -> Eff r Bool
+doesFileExist fp = withFrozenCallStack $ do
+  info "Calling: doesFileExist"
+
+  unsafeFileSystemEff_ (D.doesFileExist fp)
+    & trapIO @IOException throw
+
+doesDirectoryExist :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Log Text
+  => FilePath
+  -> Eff r Bool
+doesDirectoryExist fp = withFrozenCallStack $ do
+  info "Calling: doesDirectoryExist"
+
+  unsafeFileSystemEff_ (D.doesDirectoryExist fp)
+    & trapIO @IOException throw
+
+getCurrentDirectory :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => Eff r FilePath
+getCurrentDirectory = withFrozenCallStack do
+  unsafeFileSystemEff_ D.getCurrentDirectory
+    & trapIO @IOException throw
+{-# INLINE getCurrentDirectory #-}
+
+canonicalizePath :: ()
+  => HasCallStack
+  => r <: Error IOException
+  => r <: FileSystem
+  => FilePath
+  -> Eff r FilePath
+canonicalizePath fp = withFrozenCallStack do
+  unsafeFileSystemEff_ (D.canonicalizePath fp)
+    & trapIO @IOException throw
+{-# INLINE canonicalizePath #-}
diff --git a/components/core/Effectful/Zoo/Function.hs b/components/core/Effectful/Zoo/Function.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Function.hs
@@ -0,0 +1,26 @@
+module Effectful.Zoo.Function
+  ( once,
+  ) where
+
+import Effectful
+import Effectful.Concurrent
+import Effectful.Concurrent.MVar
+import Effectful.Zoo.Prim
+import HaskellWorks.Prelude
+
+once :: ()
+  => r <: Concurrent
+  => Eff r a
+  -> Eff r (Eff r a)
+once f = do
+  cache <- newMVar Nothing
+  return $ do
+    resultOrRunning <- takeMVar cache
+    case resultOrRunning of
+      Just result -> do
+        putMVar cache (Just result)
+        return result
+      Nothing -> do
+        result <- f
+        putMVar cache (Just result)
+        return result
diff --git a/components/core/Effectful/Zoo/Log/Api/Generic.hs b/components/core/Effectful/Zoo/Log/Api/Generic.hs
--- a/components/core/Effectful/Zoo/Log/Api/Generic.hs
+++ b/components/core/Effectful/Zoo/Log/Api/Generic.hs
@@ -11,6 +11,7 @@
 import Effectful
 import Effectful.Dispatch.Dynamic
 import Effectful.Zoo.Core
+import Effectful.Zoo.Log.Data.LogMessage
 import Effectful.Zoo.Log.Data.Severity
 import Effectful.Zoo.Log.Dynamic
 import HaskellWorks.Prelude
@@ -23,7 +24,7 @@
   -> Eff r ()
 log severity message =
   withFrozenCallStack $
-    send (Log severity message)
+    send (Log (LogMessage severity message))
 
 trace :: ()
   => HasCallStack
diff --git a/components/core/Effectful/Zoo/Log/Api/LazyText.hs b/components/core/Effectful/Zoo/Log/Api/LazyText.hs
--- a/components/core/Effectful/Zoo/Log/Api/LazyText.hs
+++ b/components/core/Effectful/Zoo/Log/Api/LazyText.hs
@@ -11,6 +11,7 @@
 import Effectful
 import Effectful.Dispatch.Dynamic
 import Effectful.Zoo.Core
+import Effectful.Zoo.Log.Data.LogMessage
 import Effectful.Zoo.Log.Data.Severity
 import Effectful.Zoo.Log.Dynamic
 import HaskellWorks.Prelude
@@ -23,7 +24,7 @@
   -> Eff r ()
 log severity message =
   withFrozenCallStack $
-    send (Log severity message)
+    send (Log (LogMessage severity message))
 
 trace :: ()
   => HasCallStack
diff --git a/components/core/Effectful/Zoo/Log/Api/String.hs b/components/core/Effectful/Zoo/Log/Api/String.hs
--- a/components/core/Effectful/Zoo/Log/Api/String.hs
+++ b/components/core/Effectful/Zoo/Log/Api/String.hs
@@ -11,6 +11,7 @@
 import Effectful
 import Effectful.Dispatch.Dynamic
 import Effectful.Zoo.Core
+import Effectful.Zoo.Log.Data.LogMessage
 import Effectful.Zoo.Log.Data.Severity
 import Effectful.Zoo.Log.Dynamic
 import HaskellWorks.Prelude
@@ -23,7 +24,7 @@
   -> Eff r ()
 log severity message =
   withFrozenCallStack $
-    send (Log severity message)
+    send (Log (LogMessage severity message))
 
 trace :: ()
   => HasCallStack
diff --git a/components/core/Effectful/Zoo/Log/Api/Text.hs b/components/core/Effectful/Zoo/Log/Api/Text.hs
--- a/components/core/Effectful/Zoo/Log/Api/Text.hs
+++ b/components/core/Effectful/Zoo/Log/Api/Text.hs
@@ -12,6 +12,7 @@
 import Effectful.Dispatch.Dynamic
 import Effectful.Zoo.Core
 import Effectful.Zoo.Log.Data.Severity
+import Effectful.Zoo.Log.Data.LogMessage
 import Effectful.Zoo.Log.Dynamic
 import HaskellWorks.Prelude
 
@@ -23,7 +24,7 @@
   -> Eff r ()
 log severity message =
   withFrozenCallStack $
-    send (Log severity message)
+    send (Log (LogMessage severity message))
 
 trace :: ()
   => HasCallStack
diff --git a/components/core/Effectful/Zoo/Log/Data/LogMessage.hs b/components/core/Effectful/Zoo/Log/Data/LogMessage.hs
--- a/components/core/Effectful/Zoo/Log/Data/LogMessage.hs
+++ b/components/core/Effectful/Zoo/Log/Data/LogMessage.hs
@@ -8,4 +8,4 @@
   { severity :: !Severity
   , message :: i
   }
-  deriving stock (Eq, Generic, Show)
+  deriving stock (Eq, Functor, Generic, Show)
diff --git a/components/core/Effectful/Zoo/Log/Data/Logger.hs b/components/core/Effectful/Zoo/Log/Data/Logger.hs
--- a/components/core/Effectful/Zoo/Log/Data/Logger.hs
+++ b/components/core/Effectful/Zoo/Log/Data/Logger.hs
@@ -5,21 +5,22 @@
 
 import Effectful
 import Effectful.Zoo.Core
-import Effectful.Zoo.Log.Data.Severity
+import Effectful.Zoo.Log.Data.LogMessage
 import HaskellWorks.Prelude
 
 newtype Logger i = Logger
-  { run :: CallStack -> Severity -> i -> IO ()
+  { run :: CallStack -> LogMessage i -> IO ()
   } deriving stock Generic
 
 instance Contravariant Logger where
-  contramap f (Logger g) = Logger \cs severity -> g cs severity . f
+  contramap f (Logger g) =
+    Logger \cs m -> g cs (fmap f m)
 
 mkLogger :: ()
   => r <: IOE
   => UnliftStrategy
-  -> (CallStack -> Severity -> i -> Eff r ())
+  -> (CallStack -> LogMessage i -> Eff r ())
   -> Eff r (Logger i)
 mkLogger strategy run =
   withEffToIO strategy $ \effToIO ->
-    pure $ Logger $ \cs severity i -> effToIO $ run cs severity i
+    pure $ Logger $ \cs m -> effToIO $ run cs m
diff --git a/components/core/Effectful/Zoo/Log/Dynamic.hs b/components/core/Effectful/Zoo/Log/Dynamic.hs
--- a/components/core/Effectful/Zoo/Log/Dynamic.hs
+++ b/components/core/Effectful/Zoo/Log/Dynamic.hs
@@ -6,15 +6,14 @@
 import Effectful
 import Effectful.Dispatch.Dynamic
 import Effectful.Zoo.Core
-import Effectful.Zoo.Log.Data.Severity
+import Effectful.Zoo.Log.Data.LogMessage
 import Effectful.Zoo.Log.Static qualified as S
 import HaskellWorks.Prelude
 
 data Log i :: Effect where
   Log
     :: HasCallStack
-    => Severity
-    -> i
+    => LogMessage i
     -> Log i m ()
 
   Local
@@ -24,13 +23,13 @@
 
 type instance DispatchOf (Log a) = Dynamic
 
-runLog :: ()
+runLog :: forall i a r. ()
   => r <: IOE
   => UnliftStrategy
-  -> (CallStack -> Severity -> i -> Eff r ())
+  -> (CallStack -> LogMessage i -> Eff r ())
   -> Eff (Log i : r) a
   -> Eff r a
 runLog s run =
   reinterpret (S.runLog s run) $ \env -> \case
-    Log severity i -> S.log severity i
+    Log m -> S.log m
     Local f m -> localSeqUnlift env $ \unlift -> S.local f (unlift m)
diff --git a/components/core/Effectful/Zoo/Log/Static.hs b/components/core/Effectful/Zoo/Log/Static.hs
--- a/components/core/Effectful/Zoo/Log/Static.hs
+++ b/components/core/Effectful/Zoo/Log/Static.hs
@@ -15,7 +15,7 @@
 import Effectful.Dispatch.Static
 import Effectful.Zoo.Core
 import Effectful.Zoo.Log.Data.Logger
-import Effectful.Zoo.Log.Data.Severity
+import Effectful.Zoo.Log.Data.LogMessage
 import GHC.Stack qualified as GHC
 import HaskellWorks.Prelude
 import System.IO qualified as IO
@@ -30,7 +30,7 @@
   => r <: IOE
   => HasCallStack
   => UnliftStrategy
-  -> (CallStack -> Severity -> i -> Eff r ())
+  -> (CallStack -> LogMessage i -> Eff r ())
   -> Eff (Log i : r) a
   -> Eff r a
 runLog strategy run f = do
@@ -40,16 +40,16 @@
 runLogToHandle :: ()
   => HasCallStack
   => Handle
-  -> (Severity -> a -> Text)
+  -> (LogMessage a -> Text)
   -> Eff (Log a : r) a
   -> Eff r a
 runLogToHandle h f =
-  evalStaticRep $ Log $ Logger $ \_ severity i ->
-    T.hPutStrLn h $ f severity i
+  evalStaticRep $ Log $ Logger $ \_ m ->
+    T.hPutStrLn h $ f m
 
 runLogToStdout :: ()
   => HasCallStack
-  => (Severity -> a -> Text)
+  => (LogMessage a -> Text)
   -> Eff (Log a : r) a
   -> Eff r a
 runLogToStdout =
@@ -57,7 +57,7 @@
 
 runLogToStderr :: ()
   => HasCallStack
-  => (Severity -> a -> Text)
+  => (LogMessage a -> Text)
   -> Eff (Log a : r) a
   -> Eff r a
 runLogToStderr =
@@ -93,13 +93,12 @@
   => HasCallStack
   => r <: Log i
   => r <: IOE
-  => Severity
-  -> i
+  => LogMessage i
   -> Eff r ()
-log severity i =
+log m =
   withFrozenCallStack do
     dataLogger <- getDataLogger
-    liftIO $ dataLogger.run GHC.callStack severity i
+    liftIO $ dataLogger.run GHC.callStack m
 
 local :: ()
   => HasCallStack
diff --git a/components/core/Effectful/Zoo/Prim.hs b/components/core/Effectful/Zoo/Prim.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Prim.hs
@@ -0,0 +1,12 @@
+{-# OPTIONS_GHC -fno-warn-deprecations #-}
+
+module Effectful.Zoo.Prim
+  ( type (<:),
+    type (<<:)
+  ) where
+
+import           Effectful ((:>), (:>>))
+
+type (<:) r e = (:>) e r
+
+type (<<:) r e = (:>>) e r
diff --git a/components/core/Effectful/Zoo/Process.hs b/components/core/Effectful/Zoo/Process.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Process.hs
@@ -0,0 +1,273 @@
+module Effectful.Zoo.Process
+  ( IO.CreateProcess(..),
+    IO.CmdSpec(..),
+    IO.StdStream(..),
+    Handle,
+    ProcessHandle,
+    ExitCode(..),
+    FD,
+    Pid,
+    createProcess,
+    createProcess_,
+    IO.shell,
+    IO.proc,
+    callProcess,
+    callCommand,
+    spawnProcess,
+    spawnCommand,
+    readCreateProcess,
+    readProcess,
+    readCreateProcessWithExitCode,
+    readProcessWithExitCode,
+    cleanupProcess,
+    getPid,
+    getCurrentPid,
+    interruptProcessGroupOf,
+    createPipe,
+    createPipeFd,
+    runProcess,
+    runCommand,
+    runInteractiveProcess,
+    runInteractiveCommand,
+    system,
+    rawSystem,
+
+    waitSecondsForProcess,
+  ) where
+
+import Control.Exception qualified as CE
+import HaskellWorks.Error
+import HaskellWorks.Error.Types
+import HaskellWorks.IO.Process qualified as IO
+import HaskellWorks.Prelude
+import Effectful
+import Effectful.Zoo.Core
+import Effectful.Zoo.Error.Static
+import System.Exit (ExitCode (..))
+import System.Posix.Internals (FD)
+import System.Process (Pid, ProcessHandle)
+import System.Process qualified as IO
+
+createProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => IO.CreateProcess
+  -> Eff r (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
+createProcess cp = do
+  r <- liftIO $ CE.try @IOException $ IO.createProcess cp
+  fromEither r
+
+createProcess_ :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> IO.CreateProcess
+  -> Eff r (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
+createProcess_ cmd cp = do
+  r <- liftIO $ CE.try @IOException $ IO.createProcess_ cmd cp
+  fromEither r
+
+callProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> [String]
+  -> Eff r ()
+callProcess cmd args = do
+  r <- liftIO $ CE.try @IOException $ IO.callProcess cmd args
+  fromEither r
+
+callCommand :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> Eff r ()
+callCommand cmd = do
+  r <- liftIO $ CE.try @IOException $ IO.callCommand cmd
+  fromEither r
+
+spawnProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> [String]
+  -> Eff r ProcessHandle
+spawnProcess cmd args = do
+  r <- liftIO $ CE.try @IOException $ IO.spawnProcess cmd args
+  fromEither r
+
+spawnCommand :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> Eff r ProcessHandle
+spawnCommand cmd = do
+  r <- liftIO $ CE.try @IOException $ IO.spawnCommand cmd
+  fromEither r
+
+readCreateProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => IO.CreateProcess
+  -> String
+  -> Eff r String
+readCreateProcess cp input = do
+  r <- liftIO $ CE.try @IOException $ IO.readCreateProcess cp input
+  fromEither r
+
+readProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> [String]
+  -> String
+  -> Eff r String
+readProcess cmd args input = do
+  r <- liftIO $ CE.try @IOException $ IO.readProcess cmd args input
+  fromEither r
+
+readCreateProcessWithExitCode :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => IO.CreateProcess
+  -> String
+  -> Eff r (ExitCode, String, String)
+readCreateProcessWithExitCode cp input = do
+  r <- liftIO $ CE.try @IOException $ IO.readCreateProcessWithExitCode cp input
+  fromEither r
+
+readProcessWithExitCode :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> [String]
+  -> String
+  -> Eff r (ExitCode, String, String)
+readProcessWithExitCode cmd args input = do
+  r <- liftIO $ CE.try @IOException $ IO.readProcessWithExitCode cmd args input
+  fromEither r
+
+cleanupProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
+  -> Eff r ()
+cleanupProcess (mIn, mOut, mErr, ph) = do
+  r <- liftIO $ CE.try @IOException $ IO.cleanupProcess (mIn, mOut, mErr, ph)
+  fromEither r
+
+getPid :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => ProcessHandle
+  -> Eff r (Maybe Pid)
+getPid ph = do
+  r <- liftIO $ CE.try @IOException $ IO.getPid ph
+  fromEither r
+
+getCurrentPid :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => Eff r Pid
+getCurrentPid = do
+  r <- liftIO $ CE.try @IOException $ IO.getCurrentPid
+  fromEither r
+
+interruptProcessGroupOf :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => ProcessHandle
+  -> Eff r ()
+interruptProcessGroupOf ph = do
+  r <- liftIO $ CE.try @IOException $ IO.interruptProcessGroupOf ph
+  fromEither r
+
+createPipe :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => Eff r (Handle, Handle)
+createPipe = do
+  r <- liftIO $ CE.try @IOException $ IO.createPipe
+  fromEither r
+
+createPipeFd :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => Eff r (FD, FD)
+createPipeFd = do
+  r <- liftIO $ CE.try @IOException $ IO.createPipeFd
+  fromEither r
+
+runProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => FilePath
+  -> [String]
+  -> Maybe FilePath
+  -> Maybe [(String, String)]
+  -> Maybe Handle
+  -> Maybe Handle
+  -> Maybe Handle
+  -> Eff r ProcessHandle
+runProcess cmd args mbStdIn mbEnv mbCwd mbStdOut mbStdErr = do
+  r <- liftIO $ CE.try @IOException $ IO.runProcess cmd args mbStdIn mbEnv mbCwd mbStdOut mbStdErr
+  fromEither r
+
+runCommand :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> Eff r ProcessHandle
+runCommand cmd = do
+  r <- liftIO $ CE.try @IOException $ IO.runCommand cmd
+  fromEither r
+
+runInteractiveProcess :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => FilePath
+  -> [String]
+  -> Maybe FilePath
+  -> Maybe [(String, String)]
+  -> Eff r (Handle, Handle, Handle, ProcessHandle)
+runInteractiveProcess cmd args mbCwd mbEnv = do
+  r <- liftIO $ CE.try @IOException $ IO.runInteractiveProcess cmd args mbCwd mbEnv
+  fromEither r
+
+runInteractiveCommand :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> Eff r (Handle, Handle, Handle, ProcessHandle)
+runInteractiveCommand cmd = do
+  r <- liftIO $ CE.try @IOException $ IO.runInteractiveCommand cmd
+  fromEither r
+
+system :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> Eff r ExitCode
+system cmd = do
+  r <- liftIO $ CE.try @IOException $ IO.system cmd
+  fromEither r
+
+rawSystem :: ()
+  => r <: Error IOException
+  => r <: IOE
+  => String
+  -> [String]
+  -> Eff r ExitCode
+rawSystem cmd args = do
+  r <- liftIO $ CE.try @IOException $ IO.rawSystem cmd args
+  fromEither r
+
+waitSecondsForProcess :: ()
+  => r <: Error TimedOut
+  => r <: IOE
+  => Int
+  -> ProcessHandle
+  -> Eff r (Maybe ExitCode)
+waitSecondsForProcess seconds hProcess =
+  liftIO (IO.waitSecondsForProcess seconds hProcess)
+    & onLeftM @TimedOut throw
diff --git a/components/core/Effectful/Zoo/Reader/Static.hs b/components/core/Effectful/Zoo/Reader/Static.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Reader/Static.hs
@@ -0,0 +1,21 @@
+module Effectful.Zoo.Reader.Static
+  ( Reader,
+    ask,
+    asks,
+    local,
+    runReader,
+
+    runReaderM,
+  ) where
+
+import Effectful
+import Effectful.Reader.Static
+import HaskellWorks.Prelude
+
+runReaderM ::
+    forall i r a.
+    Eff r i ->
+    Eff (Reader i ': r) a ->
+    Eff r a
+runReaderM f action = do
+  f >>= flip runReader action
diff --git a/components/core/Effectful/Zoo/Resource.hs b/components/core/Effectful/Zoo/Resource.hs
new file mode 100644
--- /dev/null
+++ b/components/core/Effectful/Zoo/Resource.hs
@@ -0,0 +1,6 @@
+module Effectful.Zoo.Resource
+  ( Resource,
+    runResource,
+  ) where
+
+import Effectful.Resource
diff --git a/components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/HedgehogTest.hs b/components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/HedgehogTest.hs
deleted file mode 100644
--- a/components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/HedgehogTest.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Effectful.Zoo.Hedgehog.Test.HedgehogTest where
-
-import Effectful
-import Effectful.Zoo.Core
-import Effectful.Zoo.Log.Dynamic
-import Effectful.Zoo.Log.Api.Text
-import Effectful.Zoo.Hedgehog
-import HaskellWorks.Prelude
-
-foo :: ()
-  => HasCallStack
-  => r <: Log Text
-  => Eff r ()
-foo =
-  info "This is a log"
-
-test_simple :: UnitTest
-test_simple =
-  hedgehog do
-    jot_ "This is a jot"
-
-    foo
diff --git a/components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/PropertySpec.hs b/components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/PropertySpec.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/PropertySpec.hs
@@ -0,0 +1,23 @@
+{- HLINT ignore "Use camelCase" -}
+
+module Effectful.Zoo.Hedgehog.Test.PropertySpec where
+
+import Effectful.Zoo.Hedgehog
+import Effectful.Zoo.Hedgehog.Effect.Run
+import HaskellWorks.Prelude
+import Hedgehog qualified as H
+import Hedgehog.Gen qualified as G
+import Hedgehog.Range qualified as R
+
+tasty_property_spec :: PropertyT IO ()
+tasty_property_spec = property do
+  a <- forAll $ G.int (R.linear 0 100)
+  True === True
+  a === a
+  H.success
+
+tasty_unit_spec :: TestT IO ()
+tasty_unit_spec = unit do
+  True === True
+  True === True
+  H.success
diff --git a/components/hedgehog-test/Main.hs b/components/hedgehog-test/Main.hs
--- a/components/hedgehog-test/Main.hs
+++ b/components/hedgehog-test/Main.hs
@@ -1,16 +1,1 @@
-module Main where
-
-import Effectful.Zoo.Hedgehog (unitTest)
-import Effectful.Zoo.Hedgehog.Test.HedgehogTest (test_simple)
-import Test.Tasty (TestTree, defaultMain, testGroup)
-import HaskellWorks.Prelude
-
-tests :: TestTree
-tests =
-  testGroup "all" [
-    unitTest "Simple test" test_simple
-  ]
-
-main :: IO ()
-main =
-  defaultMain tests
+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api.hs
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Api.hs
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api.hs
@@ -1,15 +1,17 @@
 module Effectful.Zoo.Hedgehog.Api
-  ( module Effectful.Zoo.Hedgehog.Api.Assert,
-    module Effectful.Zoo.Hedgehog.Api.Classify,
-    module Effectful.Zoo.Hedgehog.Api.Eval,
+  ( Property,
+    PropertyT,
+    Test,
+    TestT,
+    module Effectful.Zoo.Hedgehog.Api.Assert,
+    module Effectful.Zoo.Hedgehog.Api.Hedgehog,
     module Effectful.Zoo.Hedgehog.Api.Failure,
     module Effectful.Zoo.Hedgehog.Api.Journal,
-    module Effectful.Zoo.Hedgehog.Api.Run,
   ) where
 
 import Effectful.Zoo.Hedgehog.Api.Assert
-import Effectful.Zoo.Hedgehog.Api.Classify
-import Effectful.Zoo.Hedgehog.Api.Eval
 import Effectful.Zoo.Hedgehog.Api.Failure
+import Effectful.Zoo.Hedgehog.Api.Hedgehog
 import Effectful.Zoo.Hedgehog.Api.Journal
-import Effectful.Zoo.Hedgehog.Api.Run
+import Effectful.Zoo.Hedgehog.Api.Tasty.Orphans ()
+import Hedgehog
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Assert.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Assert.hs
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Assert.hs
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Assert.hs
@@ -1,94 +1,218 @@
 module Effectful.Zoo.Hedgehog.Api.Assert
-  ( assert,
-    (===),
-    (/==),
-    onNothingFail,
+  ( onNothingFail,
     onNothingFailM,
     onLeftFail,
     onLeftFailM,
+
+    trapFail,
+    trapFailJson,
+    trapFailJsonPretty,
+    trapFailYaml,
+
+    failMessage,
+    failWithCustom,
+
+    byDurationM,
+    byDeadlineM,
   ) where
 
+import Data.Aeson
+import Data.Aeson qualified as J
+import Data.Aeson.Encode.Pretty qualified as J
+import Data.Text qualified as T
+import Data.Text.Encoding qualified as T
+import Data.Text.Lazy qualified as LT
+import Data.Text.Lazy.Encoding qualified as LT
+import Data.Time.Clock (NominalDiffTime, UTCTime)
+import Data.Time.Clock qualified as DTC
+import Data.Yaml qualified as Y
 import Effectful
+import Effectful.Concurrent
 import Effectful.Dispatch.Dynamic
 import Effectful.Zoo.Core
-import Effectful.Zoo.Hedgehog.Api.Failure
-import Effectful.Zoo.Hedgehog.Dynamic
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Hedgehog.Api.Hedgehog
+import Effectful.Zoo.Hedgehog.Api.Journal
+import Effectful.Zoo.Hedgehog.Api.MonadAssertion
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
+import GHC.Stack qualified as GHC
 import HaskellWorks.Prelude
+import Hedgehog (MonadTest(..))
 import Hedgehog qualified as H
-
-infix 4 ===, /==
-
-assert :: forall r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => Bool
-  -> Eff r ()
-assert condition =
-  withFrozenCallStack $
-    H.assert condition
-
-(===) :: forall a r. ()
-  => HasCallStack
-  => Eq a
-  => Show a
-  => r <: Hedgehog
-  => a
-  -> a
-  -> Eff r ()
-(===) a b =
-  withFrozenCallStack $
-    a H.=== b
-
-(/==) :: forall a r. ()
-  => HasCallStack
-  => Eq a
-  => Show a
-  => r <: Hedgehog
-  => a
-  -> a
-  -> Eff r ()
-(/==) a b =
-  withFrozenCallStack $
-    a H./== b
+import Hedgehog.Internal.Property qualified as H
+import Hedgehog.Internal.Source qualified as H
 
-onNothingFail :: forall a r. ()
+onNothingFail :: forall a m. ()
   => HasCallStack
-  => r <: Hedgehog
+  => MonadTest m
   => Maybe a
-  -> Eff r a
+  -> m a
 onNothingFail mv =
   withFrozenCallStack $
     case mv of
       Just a -> pure a
       Nothing -> failWith Nothing "Expected Just, but got Nothing"
 
-onNothingFailM :: forall a r. ()
+onNothingFailM :: forall a m. ()
   => HasCallStack
-  => r <: Hedgehog
-  => Eff r (Maybe a)
-  -> Eff r a
+  => MonadTest m
+  => m (Maybe a)
+  -> m a
 onNothingFailM f =
   withFrozenCallStack $
     f >>= onNothingFail
 
-onLeftFail :: forall e a r. ()
+onLeftFail :: forall e a m. ()
   => HasCallStack
+  => MonadTest m
   => Show e
-  => r <: Hedgehog
   => Either e a
-  -> Eff r a
+  -> m a
 onLeftFail ea =
   withFrozenCallStack $
     case ea of
       Right a -> pure a
       Left e -> failWith Nothing $ "Expected Just, but got Left: " <> show e
 
-onLeftFailM :: forall e a r. ()
+onLeftFailM :: forall e a m. ()
   => HasCallStack
+  => MonadTest m
   => Show e
-  => r <: Hedgehog
-  => Eff r (Either e a)
-  -> Eff r a
+  => m (Either e a)
+  -> m a
 onLeftFailM f =
   withFrozenCallStack $
     f >>= onLeftFail
+
+failMessage :: ()
+  => H.MonadTest m
+  => CallStack
+  -> String
+  -> m a
+failMessage cs =
+  withFrozenCallStack $
+    failWithCustom cs Nothing
+
+failWithCustom :: ()
+  => H.MonadTest m
+  => CallStack
+  -> Maybe H.Diff
+  -> String
+  -> m a
+failWithCustom cs mdiff msg =
+  H.liftTest $ H.mkTest (Left $ H.Failure (H.getCaller cs) msg mdiff, mempty)
+
+trapFail :: forall e a r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error H.Failure
+  => r <: Hedgehog
+  => Show e
+  => Eff (Error e ': r) a
+  -> Eff r a
+trapFail f =
+  withFrozenCallStack do
+    r <- f & runError_
+    case r of
+      Right a -> pure a
+      Left e  -> failWith Nothing $ show e
+
+trapFailJson :: forall e a r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error H.Failure
+  => r <: Hedgehog
+  => ToJSON e
+  => Eff (Error e ': r) a
+  -> Eff r a
+trapFailJson f =
+  withFrozenCallStack do
+    r <- withFrozenCallStack $ f & runError_
+    case r of
+      Right a -> pure a
+      Left e  -> do
+        let msg = LT.unpack $ LT.decodeUtf8 $ J.encode e
+        failWith Nothing msg
+
+trapFailJsonPretty :: forall e a r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error H.Failure
+  => r <: Hedgehog
+  => ToJSON e
+  => Eff (Error e ': r) a
+  -> Eff r a
+trapFailJsonPretty f =
+  withFrozenCallStack do
+    r <- withFrozenCallStack $ f & runError_
+    case r of
+      Right a -> pure a
+      Left e  -> do
+        let msg = LT.unpack $ LT.decodeUtf8 $ J.encodePretty e
+        failWith Nothing msg
+
+trapFailYaml :: forall e a r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error H.Failure
+  => r <: Hedgehog
+  => ToJSON e
+  => Eff (Error e ': r) a
+  -> Eff r a
+trapFailYaml f =
+  withFrozenCallStack do
+    r <- withFrozenCallStack $ f & runError_
+    case r of
+      Right a -> pure a
+      Left e  -> do
+        let msg = T.unpack $ T.decodeUtf8 $ Y.encode e
+        failWith Nothing msg
+
+-- | Run the operation 'f' once a second until it returns 'True' or the deadline expires.
+--
+-- Expiration of the deadline results in an assertion failure
+byDeadlineM :: forall a r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error H.Failure
+  => r <: Hedgehog
+  => r <: IOE
+  => NominalDiffTime
+  -> UTCTime
+  -> String
+  -> Eff r a
+  -> Eff r a
+byDeadlineM period deadline errorMessage f = GHC.withFrozenCallStack $ do
+  start <- liftIO DTC.getCurrentTime
+  a <- goM
+  end <- liftIO DTC.getCurrentTime
+  jot_ $ "Operation completed in " <> tshow (DTC.diffUTCTime end start)
+  return a
+  where goM = catchAssertion f $ \e -> do
+          currentTime <- liftIO DTC.getCurrentTime
+          if currentTime < deadline
+            then do
+              threadDelay (floor (DTC.nominalDiffTimeToSeconds period * 1000000))
+              goM
+            else do
+              jotShow_ currentTime
+              void $ failMessage GHC.callStack $ "Condition not met by deadline: " <> errorMessage
+              throwAssertion e
+
+-- | Run the operation 'f' once a second until it returns 'True' or the duration expires.
+--
+-- Expiration of the duration results in an assertion failure
+byDurationM :: forall b r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error H.Failure
+  => r <: IOE
+  => r <: Hedgehog
+  => NominalDiffTime
+  -> NominalDiffTime
+  -> String
+  -> Eff r b
+  -> Eff r b
+byDurationM period duration errorMessage f = GHC.withFrozenCallStack $ do
+  deadline <- DTC.addUTCTime duration <$> liftIO DTC.getCurrentTime
+  byDeadlineM period deadline errorMessage f
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Classify.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Classify.hs
deleted file mode 100644
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Classify.hs
+++ /dev/null
@@ -1,20 +0,0 @@
-module Effectful.Zoo.Hedgehog.Api.Classify
-  ( classify,
-  ) where
-
-import Effectful
-import Effectful.Dispatch.Dynamic
-import Effectful.Zoo.Core
-import Effectful.Zoo.Hedgehog.Dynamic
-import HaskellWorks.Prelude
-import Hedgehog qualified as H
-
-classify :: forall r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => H.LabelName
-  -> Bool
-  -> Eff r ()
-classify name b =
-  withFrozenCallStack $
-    H.classify name b
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Eval.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Eval.hs
deleted file mode 100644
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Eval.hs
+++ /dev/null
@@ -1,64 +0,0 @@
-module Effectful.Zoo.Hedgehog.Api.Eval
-  ( eval,
-    evalIO,
-    evalM,
-
-    evalEither,
-    evalMaybe,
-  ) where
-
-import Effectful
-import Effectful.Dispatch.Dynamic
-import Effectful.Zoo.Core
-import Effectful.Zoo.Hedgehog.Dynamic
-import HaskellWorks.Prelude
-import Hedgehog qualified as H
-
-eval :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => a
-  -> Eff r a
-eval a =
-  withFrozenCallStack do
-    H.eval a
-
--- |Embeds 'Hedgehog.evalEither'.
-evalEither :: forall a e r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => Show e
-  => Either e a
-  -> Eff r a
-evalEither e =
-  withFrozenCallStack do
-    H.evalEither e
-
--- |Embeds 'Hedgehog.evalMaybe'.
-evalMaybe :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => Maybe a
-  -> Eff r a
-evalMaybe e =
-  withFrozenCallStack do
-    H.evalMaybe e
-
-evalIO :: forall a r. ()
-  => HasCallStack
-  => r <: IOE
-  => r <: Hedgehog
-  => IO a
-  -> Eff r a
-evalIO f =
-  withFrozenCallStack do
-    H.evalIO f
-
-evalM :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => Eff r a
-  -> Eff r a
-evalM f =
-  withFrozenCallStack do
-    H.evalM f
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Failure.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Failure.hs
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Failure.hs
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Failure.hs
@@ -1,74 +1,21 @@
 module Effectful.Zoo.Hedgehog.Api.Failure
-  ( failure,
-    failException,
-    failWith,
+  ( H.Failure,
     failWithCallStack,
-
-    catchAssertion,
-    throwAssertion,
   ) where
 
 import Effectful
-import Effectful.Dispatch.Dynamic
-import Effectful.Error.Static
 import Effectful.Zoo.Core
-import Effectful.Zoo.Hedgehog.Dynamic
+import Effectful.Zoo.Error.Static
 import HaskellWorks.Prelude
 import Hedgehog.Internal.Property qualified as H
 import Hedgehog.Internal.Source qualified as H
 
-failure :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => Eff r a
-failure =
-  withFrozenCallStack
-    H.failure
-
-failException :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => SomeException
-  -> Eff r a
-failException e =
-  withFrozenCallStack $
-    H.failException e
-
-failWith :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => Maybe H.Diff
-  -> String
-  -> Eff r a
-failWith diff msg =
-  withFrozenCallStack $
-    H.failWith diff msg
-
 failWithCallStack :: forall a r. ()
-  => r <: Hedgehog
+  => r <: Error H.Failure
   => CallStack
   -> Maybe H.Diff
   -> String
   -> Eff r a
 failWithCallStack cs diff msg =
   withFrozenCallStack $
-    throwAssertion (H.Failure (H.getCaller cs) msg diff)
-
-catchAssertion :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => Eff r a 
-  -> (H.Failure -> Eff r a) 
-  -> Eff r a
-catchAssertion m h =
-  withFrozenCallStack $
-    send $ CatchAssertion m h
-
-throwAssertion :: forall a r. ()
-  => HasCallStack
-  => r <: Hedgehog
-  => H.Failure
-  -> Eff r a
-throwAssertion e =
-  withFrozenCallStack $
-    send $ ThrowAssertion e
+    throw (H.Failure (H.getCaller cs) msg diff)
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen.hs
@@ -0,0 +1,7 @@
+module Effectful.Zoo.Hedgehog.Api.Gen
+  ( module Hedgehog.Gen,
+    module Effectful.Zoo.Hedgehog.Api.Gen.Ulid,
+  ) where
+
+import Effectful.Zoo.Hedgehog.Api.Gen.Ulid
+import Hedgehog.Gen
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Time.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Time.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Time.hs
@@ -0,0 +1,18 @@
+module Effectful.Zoo.Hedgehog.Api.Gen.Time
+  ( genPosixTime,
+  ) where
+
+import Data.Time.Clock.POSIX (POSIXTime)
+import HaskellWorks.Prelude
+import Hedgehog
+import Hedgehog.Gen qualified as Gen
+import Hedgehog.Range qualified as Range
+
+genPosixTime :: Gen POSIXTime
+genPosixTime = do
+  -- Generate a random integer within a reasonable range for POSIX time
+  -- POSIXTime is a type synonym for NominalDiffTime, which is in seconds
+  -- We'll use a range from 0 to a large number of seconds to cover a wide time span
+  seconds <- Gen.integral (Range.linear 0 4_102_444_800) -- Up to year 2100
+
+  pure $ fromIntegral @Word64 seconds
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Ulid.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Ulid.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Ulid.hs
@@ -0,0 +1,32 @@
+module Effectful.Zoo.Hedgehog.Api.Gen.Ulid (
+    genUlid,
+    genUlidRandom,
+    genUlidTimeStamp,
+) where
+
+import Data.Binary (decodeOrFail)
+import Data.ByteString.Lazy qualified as LBS
+import Data.ULID (ULID (..))
+import Data.ULID.Random (ULIDRandom)
+import Data.ULID.TimeStamp (ULIDTimeStamp, mkULIDTimeStamp)
+import Effectful.Zoo.Hedgehog.Api.Gen.Time
+import HaskellWorks.Prelude
+import Hedgehog
+import Hedgehog.Gen qualified as Gen
+import Hedgehog.Range qualified as Range
+
+genUlidRandom :: Gen ULIDRandom
+genUlidRandom = do
+  bytes <- Gen.bytes (Range.singleton 10) -- 80 bits
+  let lazyBytes = LBS.fromStrict bytes
+  case decodeOrFail lazyBytes of
+    Left (_, _, err)   -> fail $ "Failed to decode ULIDRandom: " <> err -- This shouldn't happen.
+    Right (_, _, ulid) -> pure ulid
+
+genUlidTimeStamp :: Gen ULIDTimeStamp
+genUlidTimeStamp =
+  mkULIDTimeStamp <$> genPosixTime
+
+genUlid :: Gen ULID
+genUlid =
+  ULID <$> genUlidTimeStamp <*> genUlidRandom
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Hedgehog.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Hedgehog.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Hedgehog.hs
@@ -0,0 +1,24 @@
+module Effectful.Zoo.Hedgehog.Api.Hedgehog
+  ( H.classify,
+
+    H.eval,
+    H.evalIO,
+    H.evalM,
+
+    H.evalEither,
+    H.evalMaybe,
+
+    H.assert,
+    (H.===),
+    (H./==),
+
+    H.failure,
+    H.failException,
+    H.failWith,
+
+    H.success,
+
+  ) where
+
+import Hedgehog qualified as H
+import Hedgehog.Internal.Property qualified as H
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal.hs
@@ -0,0 +1,90 @@
+module Effectful.Zoo.Hedgehog.Api.Internal.Cabal
+  ( findDefaultPlanJsonFile,
+    getPlanJsonFile,
+    binDist,
+  ) where
+
+import Data.Aeson
+import Data.List qualified as L
+import Data.Text qualified as T
+import Effectful
+import Effectful.Zoo.Core
+import Effectful.Zoo.Environment
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.FileSystem
+import Effectful.Zoo.Hedgehog.Api.Internal.Cabal.Types
+import Effectful.Zoo.Hedgehog.Api.Internal.FilePath
+import Effectful.Zoo.Log.Dynamic
+import HaskellWorks.Error.Types
+import HaskellWorks.Prelude
+import System.FilePath (takeDirectory, (</>))
+
+-- | Find the nearest plan.json going upwards from the current directory.
+findDefaultPlanJsonFile :: ()
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: IOE
+  => r <: Log Text
+  => Eff r FilePath
+findDefaultPlanJsonFile = getCurrentDirectory >>= go
+  where go :: ()
+          => r <: Error IOException
+          => r <: FileSystem
+          => r <: IOE
+          => r <: Log Text
+          => FilePath
+          -> Eff r FilePath
+        go d = do
+          let file = d </> "dist-newstyle/cache/plan.json"
+          exists <- doesFileExist file
+          if exists
+            then return file
+            else do
+              let parent = takeDirectory d
+              if parent == d
+                then return "dist-newstyle/cache/plan.json"
+                else go parent
+
+
+getPlanJsonFile :: ()
+  => r <: Environment
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: IOE
+  => r <: Log Text
+  => Eff r FilePath
+getPlanJsonFile =  do
+  maybeBuildDir <- lookupEnvMaybe "CABAL_BUILDDIR"
+  case maybeBuildDir of
+    Just buildDir -> pure $ ".." </> T.unpack buildDir </> "cache/plan.json"
+    Nothing       -> findDefaultPlanJsonFile
+
+-- | Consult the "plan.json" generated by cabal to get the path to the executable corresponding.
+-- to a haskell package.  It is assumed that the project has already been configured and the
+-- executable has been built.
+binDist:: ()
+  => r <: Environment
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: IOE
+  => r <: Log Text
+  => String
+  -- ^ Package name
+  -> Eff r FilePath
+  -- ^ Path to executable
+binDist pkg = do
+  planJsonFile <- getPlanJsonFile
+  contents <- readLazyByteStringFile planJsonFile
+
+  case eitherDecode @Plan contents of
+    Right plan -> case L.filter matching plan.installPlan of
+      (component:_) -> case component.binFile of
+        Just bin -> return $ addExeSuffix (T.unpack bin)
+        Nothing  -> throw $ GenericError $ "Missing bin-file in " <> tshow component
+      [] -> throw $ GenericError $ "Cannot find exe " <> tshow pkg <> " in plan"
+    Left msg -> throw $ GenericError $ "Cannot decode plan: " <> T.pack msg
+  where matching :: Component -> Bool
+        matching component = case component.componentName of
+          Just name -> name == "exe:" <> T.pack pkg
+          Nothing   -> False
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal/Types.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal/Types.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal/Types.hs
@@ -0,0 +1,36 @@
+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Effectful.Zoo.Hedgehog.Api.Internal.Cabal.Types
+  ( Plan(..),
+    Component(..),
+  ) where
+
+import           Control.Applicative
+import           Data.Aeson
+import           Data.Eq
+import           Data.Function
+import           Data.Maybe
+import           Data.Text           (Text)
+import           GHC.Generics
+import           Text.Show
+
+data Component = Component
+  { componentName :: Maybe Text
+  , binFile       :: Maybe Text
+  }
+  deriving stock (Generic, Eq, Show)
+
+newtype Plan = Plan
+  { installPlan :: [Component]
+  }
+  deriving stock (Generic, Eq, Show)
+
+instance FromJSON Plan where
+  parseJSON = withObject "Plan" $ \v -> Plan
+    <$> v .: "install-plan"
+
+instance FromJSON Component where
+  parseJSON = withObject "Plan" $ \v -> Component
+    <$> v .:? "component-name"
+    <*> v .:? "bin-file"
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/FilePath.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/FilePath.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/FilePath.hs
@@ -0,0 +1,16 @@
+module Effectful.Zoo.Hedgehog.Api.Internal.FilePath
+  ( exeSuffix,
+    addExeSuffix,
+  ) where
+
+import Data.List qualified as L
+import Effectful.Zoo.Hedgehog.Api.Internal.OS qualified as OS
+import HaskellWorks.Prelude
+
+exeSuffix :: String
+exeSuffix = if OS.isWin32 then ".exe" else ""
+
+addExeSuffix :: String -> String
+addExeSuffix s = if ".exe" `L.isSuffixOf` s
+  then s
+  else s <> exeSuffix
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/OS.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/OS.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/OS.hs
@@ -0,0 +1,12 @@
+module Effectful.Zoo.Hedgehog.Api.Internal.OS
+  ( isWin32,
+  ) where
+
+import           Data.Bool
+import           Data.Eq
+import           System.Info
+
+-- | Determine if the operating system is Windows.
+isWin32 :: Bool
+isWin32 = os == "mingw32"
+{-# INLINE isWin32 #-}
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Journal.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Journal.hs
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Journal.hs
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Journal.hs
@@ -1,9 +1,15 @@
 module Effectful.Zoo.Hedgehog.Api.Journal
-  ( jot,
+  ( Failure,
+
+    jot,
     jot_,
     
     jotWithCallStack,
+    jotTextWithCallStack,
 
+    jotString,
+    jotString_,
+    jotText,
     jotText_,
     jotM,
     jotBsUtf8M,
@@ -37,6 +43,11 @@
     jotEachIO,
     jotEachIO_,
 
+    jotPkgInputFile,
+    jotPkgGoldenFile,
+    jotRootInputFile,
+    jotTempFile,
+
     jotLogTextWithCallStack,
 
     jotShowDataLog,
@@ -44,6 +55,7 @@
     writeLog,
   ) where
 
+import Control.Monad.Catch (MonadCatch)
 import Data.Aeson (ToJSON(..))
 import Data.Aeson qualified as J
 import Data.Aeson.Encode.Pretty qualified as J
@@ -55,91 +67,144 @@
 import Data.Traversable
 import Data.Yaml qualified as Y
 import Effectful
+import Effectful.Concurrent
 import Effectful.Dispatch.Dynamic
 import Effectful.Zoo.Core
 import Effectful.Zoo.DataLog.Dynamic
 import Effectful.Zoo.DataLog.Dynamic qualified as DataLog
-import Effectful.Zoo.Hedgehog.Api.Eval
-import Effectful.Zoo.Hedgehog.Api.Failure
-import Effectful.Zoo.Hedgehog.Dynamic
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Hedgehog.Api.Hedgehog
+import Effectful.Zoo.Hedgehog.Data.PackagePath
+import Effectful.Zoo.Hedgehog.Data.ProjectRoot
+import Effectful.Zoo.Hedgehog.Data.Workspace
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
 import Effectful.Zoo.Log.Data.Severity
+import Effectful.Zoo.Reader.Static
 import GHC.Stack qualified as GHC
 import HaskellWorks.Prelude
 import HaskellWorks.String
 import HaskellWorks.ToText
+import Hedgehog (MonadTest(..))
+import Hedgehog.Internal.Property (Failure)
 import Hedgehog.Internal.Property qualified as H
 import Hedgehog.Internal.Source qualified as H
 
 -- | Annotate the given string at the context supplied by the callstack.
-jotWithCallStack :: forall r. ()
-  => r <: Hedgehog
+jotWithCallStack :: forall m. ()
+  => MonadTest m
   => CallStack
   -> String
-  -> Eff r ()
+  -> m ()
 jotWithCallStack cs a =
   writeLog $ H.Annotation (H.getCaller cs) a
 
+-- | Annotate the given string at the context supplied by the callstack.
+jotTextWithCallStack :: forall m. ()
+  => MonadTest m
+  => CallStack
+  -> Text
+  -> m ()
+jotTextWithCallStack cs a =
+  writeLog $ H.Annotation (H.getCaller cs) $ T.unpack a
+
 -- | Annotate with the given string.
-jot :: forall r. ()
-  => r <: Hedgehog
+jot :: forall m. ()
   => HasCallStack
-  => String
-  -> Eff r String
+  => MonadTest m
+  => Text
+  -> m Text
 jot a =
   withFrozenCallStack do
+    jotText a
+
+-- | Annotate the given string returning unit.
+jot_ :: forall m. ()
+  => HasCallStack
+  => MonadTest m
+  => Text
+  -> m ()
+jot_ a =
+  withFrozenCallStack do
+    jotText_ a
+
+-- | Annotate with the given string.
+jotString :: forall m. ()
+  => HasCallStack
+  => MonadTest m
+  => String
+  -> m String
+jotString a =
+  withFrozenCallStack do
     !b <- eval a
     jotWithCallStack GHC.callStack b
-    return b
+    pure b
 
--- | Annotate the given string returning unit.
-jot_ :: forall r. ()
-  => r <: Hedgehog
+-- | Annotate with the given string.
+jotString_ :: forall m. ()
   => HasCallStack
+  => MonadTest m
+  => String
+  -> m ()
+jotString_ a =
+  withFrozenCallStack do
+    !b <- eval a
+    jotWithCallStack GHC.callStack b
+
+-- | Annotate the given text returning unit.
+jotText :: forall m. ()
+  => HasCallStack
+  => MonadTest m
   => Text
-  -> Eff r ()
-jot_ =
+  -> m Text
+jotText a =
   withFrozenCallStack do
-    jotText_
+    !b <- eval a
+    jotWithCallStack GHC.callStack $ T.unpack a
+    pure b
 
 -- | Annotate the given text returning unit.
-jotText_ :: forall r. ()
-  => r <: Hedgehog
+jotText_ :: forall m. ()
   => HasCallStack
+  => MonadTest m
   => Text
-  -> Eff r ()
+  -> m ()
 jotText_ a =
   withFrozenCallStack do
-    jotWithCallStack GHC.callStack $ T.unpack a
+    !b <- eval a
+    jotWithCallStack GHC.callStack $ T.unpack b
 
 -- | Annotate the given string in a monadic context.
-jotM :: forall a r. ()
-  => ToString a
-  => r <: Hedgehog
+jotM :: forall a m. ()
   => HasCallStack
-  => Eff r a
-  -> Eff r a
+  => MonadCatch m
+  => MonadTest m
+  => ToString a
+  => m a
+  -> m a
 jotM a =
   withFrozenCallStack do
     !b <- evalM a
     jotWithCallStack GHC.callStack $ toString b
     return b
 
-jotBsUtf8M :: forall r. ()
-  => r <: Hedgehog
+jotBsUtf8M :: forall m. ()
   => HasCallStack
-  => Eff r ByteString
-  -> Eff r ByteString
+  => MonadCatch m
+  => MonadTest m
+  => m ByteString
+  -> m ByteString
 jotBsUtf8M a =
   withFrozenCallStack do
     !b <- evalM a
     jotWithCallStack GHC.callStack $ T.unpack $ T.decodeUtf8 b
     return b
 
-jotLbsUtf8M :: forall r. ()
-  => r <: Hedgehog
+jotLbsUtf8M :: forall m. ()
   => HasCallStack
-  => Eff r LBS.ByteString
-  -> Eff r LBS.ByteString
+  => MonadCatch m
+  => MonadTest m
+  => m LBS.ByteString
+  -> m LBS.ByteString
 jotLbsUtf8M a =
   withFrozenCallStack do
     !b <- evalM a
@@ -147,11 +212,12 @@
     return b
 
 -- | Annotate the given string in a monadic context returning unit.
-jotM_ :: forall r. ()
-  => r <: Hedgehog
+jotM_ :: forall m. ()
   => HasCallStack
-  => Eff r String
-  -> Eff r ()
+  => MonadCatch m
+  => MonadTest m
+  => m String
+  -> m ()
 jotM_ a =
   withFrozenCallStack do
     !b <- evalM a
@@ -159,12 +225,12 @@
     return ()
 
 -- | Annotate the given string in IO.
-jotIO :: forall r. ()
-  => r <: Hedgehog
-  => r <: IOE
+jotIO :: forall m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => IO String
-  -> Eff r String
+  -> m String
 jotIO f =
   withFrozenCallStack do
     !a <- evalIO f
@@ -172,12 +238,12 @@
     return a
 
 -- | Annotate the given string in IO returning unit.
-jotIO_ :: forall r. ()
-  => r <: Hedgehog
-  => r <: IOE
+jotIO_ :: forall m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => IO String
-  -> Eff r ()
+  -> m ()
 jotIO_ f =
   withFrozenCallStack do
     !a <- evalIO f
@@ -185,12 +251,13 @@
     return ()
 
 -- | Annotate the given value.
-jotShow :: forall a r. ()
-  => r <: Hedgehog
+jotShow :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => a
-  -> Eff r a
+  -> m a
 jotShow a =
   withFrozenCallStack do
     !b <- eval a
@@ -198,23 +265,24 @@
     return b
 
 -- | Annotate the given value returning unit.
-jotShow_ :: forall a r. ()
-  => r <: Hedgehog
+jotShow_ :: forall a m. ()
   => HasCallStack
+  => MonadTest m
   => Show a
   => a
-  -> Eff r ()
+  -> m ()
 jotShow_ a =
   withFrozenCallStack do
     jotWithCallStack GHC.callStack (show a)
 
 -- | Annotate the given value in a monadic context.
-jotShowM :: forall a r. ()
-  => r <: Hedgehog
+jotShowM :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadTest m
   => Show a
-  => Eff r a
-  -> Eff r a
+  => m a
+  -> m a
 jotShowM a =
   withFrozenCallStack do
     !b <- evalM a
@@ -222,12 +290,13 @@
     return b
 
 -- | Annotate the given value in a monadic context returning unit.
-jotShowM_ :: forall a r. ()
-  => r <: Hedgehog
+jotShowM_ :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadTest m
   => Show a
-  => Eff r a
-  -> Eff r ()
+  => m a
+  -> m ()
 jotShowM_ a =
   withFrozenCallStack do
     !b <- evalM a
@@ -235,13 +304,13 @@
     return ()
 
 -- | Annotate the given value in IO.
-jotShowIO :: forall a r. ()
-  => r <: Hedgehog
-  => r <: IOE
+jotShowIO :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => IO a
-  -> Eff r a
+  -> m a
 jotShowIO f =
   withFrozenCallStack do
     !a <- evalIO f
@@ -249,13 +318,13 @@
     return a
 
 -- | Annotate the given value in IO returning unit.
-jotShowIO_ :: forall a r. ()
-  => r <: Hedgehog
-  => r <: IOE
+jotShowIO_ :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => IO a
-  -> Eff r ()
+  -> m ()
 jotShowIO_ f =
   withFrozenCallStack do
     !a <- evalIO f
@@ -263,13 +332,14 @@
     return ()
 
 -- | Annotate the given value.
-jotShowRead :: forall a r. ()
+jotShowRead :: forall a m. ()
   => HasCallStack
-  => r <: Hedgehog
+  => MonadIO m
+  => MonadTest m
   => Read a
   => Show a
   => String
-  -> Eff r a
+  -> m a
 jotShowRead s =
   withFrozenCallStack do
     !result <- eval (readEither @a s)
@@ -280,12 +350,13 @@
         return a
 
 -- | Annotate the given value as JSON.
-jotJson :: forall a r. ()
-  => r <: Hedgehog
+jotJson :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
   => a
-  -> Eff r a
+  -> m a
 jotJson a =
   withFrozenCallStack do
     !b <- eval a
@@ -293,12 +364,13 @@
     return b
 
 -- | Annotate the given value as JSON.
-jotJson_ :: forall a r. ()
-  => r <: Hedgehog
+jotJson_ :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
   => a
-  -> Eff r ()
+  -> m ()
 jotJson_ a =
   withFrozenCallStack do
     !b <- eval a
@@ -306,12 +378,14 @@
     return ()
 
 -- | Annotate the given value as JSON in a monadic context.
-jotJsonM :: forall a r. ()
-  => r <: Hedgehog
+jotJsonM :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
-  => Eff r a
-  -> Eff r a
+  => m a
+  -> m a
 jotJsonM a =
   withFrozenCallStack do
     !b <- evalM a
@@ -319,12 +393,14 @@
     return b
 
 -- | Annotate the given value as JSON in a monadic context.
-jotJsonM_ :: forall a r. ()
-  => r <: Hedgehog
+jotJsonM_ :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
-  => Eff r a
-  -> Eff r ()
+  => m a
+  -> m ()
 jotJsonM_ a =
   withFrozenCallStack do
     !b <- evalM a
@@ -332,12 +408,13 @@
     return ()
 
 -- | Annotate the given value as JSON.
-jotJsonPretty :: forall a r. ()
-  => r <: Hedgehog
+jotJsonPretty :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
   => a
-  -> Eff r a
+  -> m a
 jotJsonPretty a =
   withFrozenCallStack do
     !b <- eval a
@@ -345,12 +422,13 @@
     return b
 
 -- | Annotate the given value as JSON.
-jotJsonPretty_ :: forall a r. ()
-  => r <: Hedgehog
+jotJsonPretty_ :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
   => a
-  -> Eff r ()
+  -> m ()
 jotJsonPretty_ a =
   withFrozenCallStack do
     !b <- eval a
@@ -358,12 +436,14 @@
     return ()
 
 -- | Annotate the given value as JSON in a monadic context.
-jotJsonPrettyM :: forall a r. ()
-  => r <: Hedgehog
+jotJsonPrettyM :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
-  => Eff r a
-  -> Eff r a
+  => m a
+  -> m a
 jotJsonPrettyM a =
   withFrozenCallStack do
     !b <- evalM a
@@ -371,12 +451,14 @@
     return b
 
 -- | Annotate the given value as JSON in a monadic context.
-jotJsonPrettyM_ :: forall a r. ()
-  => r <: Hedgehog
+jotJsonPrettyM_ :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
-  => Eff r a
-  -> Eff r ()
+  => m a
+  -> m ()
 jotJsonPrettyM_ a =
   withFrozenCallStack do
     !b <- evalM a
@@ -384,12 +466,13 @@
     return ()
 
 -- | Annotate the given value as JSON.
-jotYaml :: forall a r. ()
-  => r <: Hedgehog
+jotYaml :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
   => a
-  -> Eff r a
+  -> m a
 jotYaml a =
   withFrozenCallStack do
     !b <- eval a
@@ -397,12 +480,13 @@
     return b
 
 -- | Annotate the given value as JSON.
-jotYaml_ :: forall a r. ()
-  => r <: Hedgehog
+jotYaml_ :: forall a m. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
   => a
-  -> Eff r ()
+  -> m ()
 jotYaml_ a =
   withFrozenCallStack do
     !b <- eval a
@@ -410,12 +494,14 @@
     return ()
 
 -- | Annotate the given value as JSON in a monadic context.
-jotYamlM :: forall a r. ()
-  => r <: Hedgehog
+jotYamlM :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
-  => Eff r a
-  -> Eff r a
+  => m a
+  -> m a
 jotYamlM a =
   withFrozenCallStack do
     !b <- evalM a
@@ -423,12 +509,14 @@
     return b
 
 -- | Annotate the given value as JSON in a monadic context.
-jotYamlM_ :: forall a r. ()
-  => r <: Hedgehog
+jotYamlM_ :: forall a m. ()
   => HasCallStack
+  => MonadCatch m
+  => MonadIO m
+  => MonadTest m
   => ToJSON a
-  => Eff r a
-  -> Eff r ()
+  => m a
+  -> m ()
 jotYamlM_ a =
   withFrozenCallStack do
     !b <- evalM a
@@ -436,37 +524,40 @@
     return ()
 
 -- | Annotate the each value in the given traversable.
-jotEach :: forall a f r. ()
-  => r <: Hedgehog
+jotEach :: forall a m f. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => Traversable f
   => f a
-  -> Eff r (f a)
+  -> m (f a)
 jotEach as =
   withFrozenCallStack do
     for_ as $ jotWithCallStack GHC.callStack . show
     return as
 
 -- | Annotate the each value in the given traversable returning unit.
-jotEach_ :: forall a f r. ()
-  => r <: Hedgehog
+jotEach_ :: forall a m f. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => Traversable f
   => f a
-  -> Eff r ()
+  -> m ()
 jotEach_ as =
   withFrozenCallStack $ for_ as $ jotWithCallStack GHC.callStack . show
 
 -- | Annotate the each value in the given traversable in a monadic context.
-jotEachM :: forall a f r. ()
-  => r <: Hedgehog
+jotEachM :: forall a m f. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => Traversable f
-  => Eff r (f a)
-  -> Eff r (f a)
+  => m (f a)
+  -> m (f a)
 jotEachM f =
   withFrozenCallStack do
     !as <- f
@@ -474,27 +565,28 @@
     return as
 
 -- | Annotate the each value in the given traversable in a monadic context returning unit.
-jotEachM_ :: forall a f r. ()
-  => r <: Hedgehog
+jotEachM_ :: forall a m f. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => Traversable f
-  => Eff r (f a)
-  -> Eff r ()
+  => m (f a)
+  -> m ()
 jotEachM_ f =
   withFrozenCallStack do
     !as <- f
     for_ as $ jotWithCallStack GHC.callStack . show
 
 -- | Annotate the each value in the given traversable in IO.
-jotEachIO :: forall a f r. ()
-  => r <: Hedgehog
-  => r <: IOE
+jotEachIO :: forall a m f. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => Traversable f
   => IO (f a)
-  -> Eff r (f a)
+  -> m (f a)
 jotEachIO f =
   withFrozenCallStack do
     !as <- evalIO f
@@ -502,25 +594,81 @@
     return as
 
 -- | Annotate the each value in the given traversable in IO returning unit.
-jotEachIO_ :: forall a f r. ()
-  => r <: Hedgehog
-  => r <: IOE
+jotEachIO_ :: forall a m f. ()
   => HasCallStack
+  => MonadIO m
+  => MonadTest m
   => Show a
   => Traversable f
   => IO (f a)
-  -> Eff r ()
+  -> m ()
 jotEachIO_ f =
   withFrozenCallStack do
     !as <- evalIO f
     for_ as $ jotWithCallStack GHC.callStack . show
 
-jotLogTextWithCallStack :: forall r. ()
+-- | Return the input file path after annotating it relative to the package directory
+jotPkgInputFile :: forall r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
   => r <: Hedgehog
+  => r <: Reader PackagePath
+  => FilePath
+  -> Eff r FilePath
+jotPkgInputFile fp = withFrozenCallStack $ do
+  PackagePath { filePath = pkgPath } <- ask
+  jotString_ $ pkgPath <> "/" <> fp
+  return fp
+
+-- | Return the golden file path after annotating it relative to the package directory
+jotPkgGoldenFile :: forall r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Hedgehog
+  => r <: Reader PackagePath
+  => FilePath
+  -> Eff r FilePath
+jotPkgGoldenFile fp = withFrozenCallStack $ do
+  PackagePath { filePath = pkgPath } <- ask
+  jotString_ $ pkgPath <> "/" <> fp
+  return fp
+
+jotRootInputFile :: forall r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Hedgehog
+  => r <: Reader ProjectRoot
+  => FilePath
+  -> Eff r FilePath
+jotRootInputFile fp = withFrozenCallStack $ do
+  ProjectRoot { filePath = pkgPath } <- ask
+  jotString $ pkgPath <> "/" <> fp
+
+-- | Return the test file path after annotating it relative to the project root directory
+jotTempFile :: forall r. ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Hedgehog
+  => r <: Reader Workspace
+  => FilePath
+  -> Eff r FilePath
+jotTempFile fp = withFrozenCallStack $ do
+  Workspace { filePath = workspace } <- ask
+  let relPath = workspace <> "/" <> fp
+  jotString_ $ workspace <> "/" <> relPath
+  return relPath
+
+jotLogTextWithCallStack :: forall m. ()
+  => HasCallStack
+  => MonadTest m
   => CallStack
   -> Severity
   -> Text
-  -> Eff r ()
+  -> m ()
 jotLogTextWithCallStack cs severity a =
   withFrozenCallStack do
     jotWithCallStack cs $ T.unpack $ "[" <> toText severity <> "] " <> a
@@ -528,6 +676,8 @@
 jotShowDataLog :: forall i a r. ()
   => HasCallStack
   => Show i
+  => r <: Concurrent
+  => r <: Error Failure
   => r <: Hedgehog
   => Eff (DataLog i : r) a
   -> Eff r a
@@ -536,11 +686,11 @@
     DataLog.runDataLog jotShow_
 {-# inline jotShowDataLog #-}
 
-writeLog :: forall r. ()
+writeLog :: forall m. ()
   => HasCallStack
-  => r <: Hedgehog
+  => MonadTest m
   => H.Log
-  -> Eff r ()
+  -> m ()
 writeLog message =
   withFrozenCallStack $
     H.writeLog message
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/MonadAssertion.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/MonadAssertion.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/MonadAssertion.hs
@@ -0,0 +1,47 @@
+module Effectful.Zoo.Hedgehog.Api.MonadAssertion
+  ( MonadAssertion(..),
+    tryAssertion,
+    tryExceptAssertion,
+  ) where
+
+import Control.Monad.Catch (MonadCatch(..))
+import Control.Monad.Catch qualified as C
+import Control.Monad.Trans.Class
+import Control.Monad.Trans.Except qualified as E
+import Control.Monad.Trans.Resource qualified as IO
+import Control.Monad.Trans.Resource.Internal qualified as IO
+import Effectful.Zoo.Hedgehog.Data.TestResult
+import HaskellWorks.Prelude
+import Hedgehog.Internal.Property qualified as H
+
+class Monad m => MonadAssertion m where
+  throwAssertion :: H.Failure -> m a
+  catchAssertion :: m a -> (H.Failure -> m a) -> m a
+
+instance Monad m => MonadAssertion (H.TestT m) where
+  throwAssertion f = H.liftTest $ H.mkTest (Left f, mempty)
+  catchAssertion g h = H.TestT $ E.catchE (H.unTest g) (H.unTest . h)
+
+instance MonadAssertion m => MonadAssertion (IO.ResourceT m) where
+  throwAssertion = lift . throwAssertion
+  catchAssertion r h = IO.ResourceT $ \i -> IO.unResourceT r i `catchAssertion` \e -> IO.unResourceT (h e) i
+
+deriving newtype instance Monad m => MonadAssertion (H.PropertyT m)
+
+tryAssertion :: ()
+  => MonadAssertion m
+  => m a
+  -> m (Either H.Failure a)
+tryAssertion m =
+  catchAssertion (Right <$> m) (pure . Left)
+
+tryExceptAssertion :: ()
+  => MonadAssertion m
+  => MonadCatch m
+  => m a
+  -> m (TestResult a)
+tryExceptAssertion m =
+  tryAssertion (C.try m) >>= \case
+    Right (Right a) -> pure $ TestResult a
+    Right (Left e) -> pure $ TestError e
+    Left f -> pure $ TestFailure f
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process.hs
@@ -0,0 +1,313 @@
+module Effectful.Zoo.Hedgehog.Api.Process
+  ( ExecConfig(..),
+    defaultExecConfig,
+    execDetailFlex,
+    execFlex,
+    execFlexOk,
+    execFlexOk',
+    execOk,
+    execOk_,
+    exec,
+    procFlex,
+    procFlex',
+    binFlex,
+
+    waitSecondsForProcess,
+    waitSecondsForProcessOk,
+
+  ) where
+
+import Data.List qualified as L
+import Data.Monoid (Last (..))
+import Data.Text qualified as T
+import Effectful
+import Effectful.Concurrent
+import Effectful.Zoo.Core
+import Effectful.Zoo.Environment
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Hedgehog.Api.Assert
+import Effectful.Zoo.Hedgehog.Api.Internal.Cabal
+import Effectful.Zoo.Hedgehog.Api.Journal
+import Effectful.Zoo.Hedgehog.Api.Process.Internal
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
+import Effectful.Zoo.Log.Dynamic
+import Effectful.Zoo.Process
+import GHC.Stack (callStack)
+import HaskellWorks.Error.Types
+import HaskellWorks.Prelude
+import Effectful.FileSystem (FileSystem)
+
+-- | Configuration for starting a new process.  This is a subset of 'IO.CreateProcess'.
+data ExecConfig = ExecConfig
+  { execConfigEnv :: Last [(String, String)]
+  , execConfigCwd :: Last FilePath
+  } deriving stock (Eq, Generic, Show)
+
+defaultExecConfig :: ExecConfig
+defaultExecConfig = ExecConfig
+  { execConfigEnv = mempty
+  , execConfigCwd = mempty
+  }
+
+-- | Create a process returning its stdout.
+--
+-- Being a 'flex' function means that the environment determines how the process is launched.
+--
+-- When running in a nix environment, the 'envBin' argument describes the environment variable
+-- that defines the binary to use to launch the process.
+--
+-- When running outside a nix environment, the `pkgBin` describes the name of the binary
+-- to launch via cabal exec.
+execFlexOk :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error Failure
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: IOE
+  => r <: Log Text
+  => String
+  -> String
+  -> [String]
+  -> Eff r String
+execFlexOk = execFlexOk' defaultExecConfig
+
+execFlexOk' :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error Failure
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: IOE
+  => r <: Log Text
+  => ExecConfig
+  -> String
+  -> String
+  -> [String]
+  -> Eff r String
+execFlexOk' execConfig pkgBin envBin arguments =
+  withFrozenCallStack do
+    (exitResult, stdout, stderr) <- execFlex execConfig pkgBin envBin arguments
+    case exitResult of
+      ExitFailure exitCode -> do
+        jotString_ $ L.unlines $
+          [ "Process exited with non-zero exit-code: " <> show @Int exitCode ]
+          <> (if L.null stdout then [] else ["━━━━ stdout ━━━━" , stdout])
+          <> (if L.null stderr then [] else ["━━━━ stderr ━━━━" , stderr])
+        failMessage callStack "Execute process failed"
+      ExitSuccess -> return stdout
+
+-- | Run a process, returning its exit code, its stdout, and its stderr.
+-- Contrary to @execFlexOk'@, this function doesn't fail if the call fails.
+-- So, if you want to test something negative, this is the function to use.
+execFlex :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error Failure
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: IOE
+  => r <: Log Text
+  => ExecConfig
+  -> String -- ^ @pkgBin@: name of the binary to launch via 'cabal exec'
+  -> String -- ^ @envBin@: environment variable defining the binary to launch the process, when in Nix
+  -> [String]
+  -> Eff r (ExitCode, String, String) -- ^ exit code, stdout, stderr
+execFlex execConfig pkgBin envBin arguments =
+  withFrozenCallStack do
+    cp <- procFlex' execConfig pkgBin envBin arguments
+    jotString_ . ("━━━━ command ━━━━\n" <>) $ case cmdspec cp of
+      ShellCommand cmd    -> cmd
+      RawCommand cmd args -> cmd <> " " <> L.unwords (argQuote <$> args)
+
+    readCreateProcessWithExitCode cp ""
+
+execDetailFlex :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error Failure
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: IOE
+  => r <: Log Text
+  => ExecConfig
+  -> String
+  -> String
+  -> [String]
+  -> Eff r (ExitCode, String, String)
+execDetailFlex execConfig pkgBin envBin arguments =
+  withFrozenCallStack do
+    cp <- procFlex' execConfig pkgBin envBin arguments
+    jotString_ . ("Command: " <>) $ case cmdspec cp of
+      ShellCommand cmd    -> cmd
+      RawCommand cmd args -> cmd <> " " <> L.unwords args
+    readCreateProcessWithExitCode cp ""
+
+-- | Execute a process, returning '()'.
+execOk_ :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Error IOException
+  => r <: Hedgehog
+  => r <: IOE
+  => ExecConfig
+  -> String
+  -> [String]
+  -> Eff r ()
+execOk_ execConfig bin arguments =
+  void $ execOk execConfig bin arguments
+
+-- | Execute a process, returning the stdout. Fail if the call returns
+-- with a non-zero exit code. For a version that doesn't fail upon receiving
+-- a non-zero exit code, see 'execAny'.
+execOk :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Error IOException
+  => r <: Hedgehog
+  => r <: IOE
+  => ExecConfig
+  -> String
+  -> [String]
+  -> Eff r String
+execOk execConfig bin arguments =
+  withFrozenCallStack do
+    (exitResult, stdout, stderr) <- exec execConfig bin arguments
+    case exitResult of
+      ExitFailure exitCode ->failMessage callStack . L.unlines $
+        [ "Process exited with non-zero exit-code: " <> show @Int exitCode ]
+        <> (if L.null stdout then [] else ["━━━━ stdout ━━━━" , stdout])
+        <> (if L.null stderr then [] else ["━━━━ stderr ━━━━" , stderr])
+      ExitSuccess -> return stdout
+
+-- | Execute a process, returning the error code, the stdout, and the stderr.
+exec :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Error IOException
+  => r <: Hedgehog
+  => r <: IOE
+  => ExecConfig
+  -> String -- ^ The binary to launch
+  -> [String] -- ^ The binary's arguments
+  -> Eff r (ExitCode, String, String) -- ^ exit code, stdout, stderr
+exec execConfig bin arguments =
+  withFrozenCallStack do
+    let cp = (proc bin arguments)
+          { env = getLast execConfig.execConfigEnv
+          , cwd = getLast execConfig.execConfigCwd
+          }
+    jotString_ . ( "━━━━ command ━━━━\n" <>) $ bin <> " " <> L.unwords (argQuote <$> arguments)
+    readCreateProcessWithExitCode cp ""
+
+-- | Wait a maximum of 'seconds' secons for process to exit.
+waitSecondsForProcessOk :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Hedgehog
+  => r <: IOE
+  => Int
+  -> ProcessHandle
+  -> Eff r ExitCode
+waitSecondsForProcessOk seconds hProcess =
+  withFrozenCallStack do
+    maybeExitCode <- waitSecondsForProcess seconds hProcess
+      & trapFail @TimedOut
+
+    case maybeExitCode of
+      Nothing -> failMessage callStack "No exit code for process"
+      Just exitCode -> do
+        jotString_ $ "Process exited " <> show exitCode
+        return exitCode
+
+-- | Compute the path to the binary given a package name or an environment variable override.
+binFlex :: ()
+  => HasCallStack
+  => r <: Environment
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: IOE
+  => r <: Log Text
+  => String
+  -- ^ Package name
+  -> String
+  -- ^ Environment variable pointing to the binary to run
+  -> Eff r FilePath
+  -- ^ Path to executable
+binFlex pkg binaryEnv =
+  withFrozenCallStack do
+    maybeEnvBin <- lookupEnvMaybe $ T.pack binaryEnv
+    case maybeEnvBin of
+      Just envBin -> return $ T.unpack envBin
+      Nothing     -> binDist pkg
+
+-- | Create a 'CreateProcess' describing how to start a process given the Cabal package name
+-- corresponding to the executable, an environment variable pointing to the executable,
+-- and an argument list.
+--
+-- The actual executable used will the one specified by the environment variable, but if
+-- the environment variable is not defined, it will be found instead by consulting the
+-- "plan.json" generated by cabal.  It is assumed that the project has already been
+-- configured and the executable has been built.
+procFlex :: ()
+  => HasCallStack
+  => r <: Environment
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: IOE
+  => r <: Log Text
+  => String
+  -- ^ Cabal package name corresponding to the executable
+  -> String
+  -- ^ Environment variable pointing to the binary to run
+  -> [String]
+  -- ^ Arguments to the CLI command
+  -> Eff r CreateProcess
+  -- ^ Captured stdout
+procFlex =
+  procFlex' defaultExecConfig
+
+procFlex' :: ()
+  => HasCallStack
+  => r <: Environment
+  => r <: Error GenericError
+  => r <: Error IOException
+  => r <: FileSystem
+  => r <: IOE
+  => r <: Log Text
+  => ExecConfig
+  -> String
+  -- ^ Cabal package name corresponding to the executable
+  -> String
+  -- ^ Environment variable pointing to the binary to run
+  -> [String]
+  -- ^ Arguments to the CLI command
+  -> Eff r CreateProcess
+  -- ^ Captured stdout
+procFlex' execConfig pkg binaryEnv arguments =
+  withFrozenCallStack do
+    bin <- binFlex pkg binaryEnv
+    return (proc bin arguments)
+      { env = getLast execConfig.execConfigEnv
+      , cwd = getLast execConfig.execConfigCwd
+      -- this allows sending signals to the created processes, without killing the test-suite process
+      , create_group = True
+      }
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process/Internal.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process/Internal.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process/Internal.hs
@@ -0,0 +1,28 @@
+module Effectful.Zoo.Hedgehog.Api.Process.Internal
+  ( argQuote
+  ) where
+
+import Data.Bool
+import Data.Semigroup
+import Data.String
+
+import qualified Data.List      as L
+
+-- | Format argument for a shell CLI command.
+--
+-- This includes automatically embedding string in double quotes if necessary, including any necessary escaping.
+--
+-- Note, this function does not cover all the edge cases for shell processing, so avoid use in production code.
+argQuote :: String -> String
+argQuote arg = if ' ' `L.elem` arg || '"' `L.elem` arg || '$' `L.elem` arg
+  then "\"" <> escape arg <> "\""
+  else arg
+  where escape :: String -> String
+        escape ('"':xs)  = '\\':'"':escape xs
+        escape ('\\':xs) = '\\':'\\':escape xs
+        escape ('\n':xs) = '\\':'n':escape xs
+        escape ('\r':xs) = '\\':'r':escape xs
+        escape ('\t':xs) = '\\':'t':escape xs
+        escape ('$':xs)  = '\\':'$':escape xs
+        escape (x:xs)    = x:escape xs
+        escape ""        = ""
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Range.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Range.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Range.hs
@@ -0,0 +1,5 @@
+module Effectful.Zoo.Hedgehog.Api.Range
+  ( module Hedgehog.Range,
+  ) where
+
+import Hedgehog.Range
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Run.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Run.hs
deleted file mode 100644
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Run.hs
+++ /dev/null
@@ -1,47 +0,0 @@
-module Effectful.Zoo.Hedgehog.Api.Run
-  ( UnitTest,
-
-    hedgehog,
-    unitTest,
-  ) where
-
-import Control.Monad.Trans.Writer.Lazy qualified as MTL
-import Effectful
-import Effectful.Error.Static
-import Effectful.Writer.Static.Local
-import Effectful.Zoo.Hedgehog.Api.Journal
-import Effectful.Zoo.Hedgehog.Dynamic
-import Effectful.Zoo.Log.Dynamic
-import HaskellWorks.Prelude
-import Hedgehog (TestT)
-import Hedgehog qualified as H
-import Hedgehog.Internal.Property (Failure, Journal)
-import Hedgehog.Internal.Property qualified as H
-import Test.Tasty (TestName, TestTree)
-import Test.Tasty.Hedgehog (testProperty)
-
-type UnitTest = TestT IO ()
-
-hedgehog :: forall a. ()
-  => Eff
-      [ Log Text
-      , Hedgehog
-      , Error Failure
-      , Writer Journal
-      , IOE
-      ] a
-  -> H.TestT IO a
-hedgehog f =
-  f
-    & runLog (ConcUnlift Persistent Unlimited) jotLogTextWithCallStack
-    & runHedgehogIO
-    & MTL.WriterT
-    & ExceptT
-    & H.TestT
-
-unitTest :: ()
-  => TestName 
-  -> UnitTest 
-  -> TestTree
-unitTest desc =
-  testProperty desc . H.withTests 1 . H.property . H.test
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Stack.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Stack.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Stack.hs
@@ -0,0 +1,10 @@
+module Effectful.Zoo.Hedgehog.Api.Stack
+  ( callerModuleName,
+  ) where
+
+import GHC.Stack (callStack, getCallStack, srcLocModule)
+import HaskellWorks.Prelude
+
+-- | Get the module name of the caller.
+callerModuleName :: HasCallStack => String
+callerModuleName = maybe "<no-module>" (srcLocModule . snd) (listToMaybe (getCallStack callStack))
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty.hs
@@ -0,0 +1,20 @@
+module Effectful.Zoo.Hedgehog.Api.Tasty
+  ( ToTestTree(..),
+  ) where
+
+import HaskellWorks.Prelude
+import Hedgehog (PropertyT, TestT)
+import Hedgehog qualified as H
+import Test.Tasty (TestName, TestTree)
+import Test.Tasty.Hedgehog (testProperty)
+
+class ToTestTree a where
+  toTestTree :: TestName -> a -> TestTree
+
+instance ToTestTree (PropertyT IO ()) where
+  toTestTree desc =
+    testProperty desc . H.property
+
+instance ToTestTree (TestT IO ()) where
+  toTestTree desc =
+    testProperty desc . H.withTests 1 . H.property . H.test
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty/Orphans.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty/Orphans.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty/Orphans.hs
@@ -0,0 +1,18 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Effectful.Zoo.Hedgehog.Api.Tasty.Orphans where
+
+import Data.Monoid
+import HaskellWorks.Prelude
+import Hedgehog (PropertyT, TestT)
+import Hedgehog qualified as H
+import Test.Tasty.Discover
+import Test.Tasty.Discover.TastyInfo
+import Test.Tasty.Hedgehog
+
+instance Tasty (PropertyT IO ()) where
+  tasty info = pure . testProperty testName . H.property
+    where testName = fromMaybe "" $ getLast info.name
+
+instance Tasty (TestT IO ()) where
+  tasty info = tasty info . H.test
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Workspace.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Workspace.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Api/Workspace.hs
@@ -0,0 +1,112 @@
+module Effectful.Zoo.Hedgehog.Api.Workspace
+  ( PackagePath(..),
+    ProjectRoot(..),
+    Workspace(..),
+    workspace,
+    moduleWorkspace,
+    findCabalProjectDir,
+  ) where
+
+import Data.Text qualified as T
+import Effectful
+import Effectful.Concurrent
+import Effectful.Environment
+import Effectful.Reader.Static
+import Effectful.Zoo.Core
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.FileSystem
+import Effectful.Zoo.Hedgehog.Api.Assert
+import Effectful.Zoo.Hedgehog.Api.Hedgehog
+import Effectful.Zoo.Hedgehog.Api.Journal
+import Effectful.Zoo.Hedgehog.Api.Stack
+import Effectful.Zoo.Hedgehog.Data.PackagePath
+import Effectful.Zoo.Hedgehog.Data.ProjectRoot
+import Effectful.Zoo.Hedgehog.Data.Workspace
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
+import Effectful.Zoo.Log.Dynamic
+import HaskellWorks.Prelude
+import Hedgehog.Internal.Property qualified as H
+import System.FilePath ((</>))
+import System.Info
+
+-- | Create a workspace directory which will exist for at least the duration of
+-- the supplied block.
+--
+-- The directory will have the supplied prefix but contain a generated random
+-- suffix to prevent interference between tests
+--
+-- The directory will be deleted if the block succeeds, but left behind if
+-- the block fails.
+workspace :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error H.Failure
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: Log Text
+  => FilePath
+  -> Eff (Reader Workspace : r) ()
+  -> Eff r ()
+workspace prefixPath f = withFrozenCallStack $ do
+  systemTemp <- getCanonicalTemporaryDirectory
+    & trapFail @IOException
+  maybeKeepWorkspace <- lookupEnv "KEEP_WORKSPACE"
+  ws <- createTempDirectory systemTemp (prefixPath <> "-test")
+    & trapFail @IOException
+  jot_ $ "Workspace: " <> T.pack ws
+  writeStringFile (ws </> "module") callerModuleName
+    & trapFail @IOException
+  runReader (Workspace ws) f
+  when (os /= "mingw32" && maybeKeepWorkspace /= Just "1") $ do
+    removePathForcibly ws
+      & trapFail @IOException
+
+-- | Create a workspace directory which will exist for at least the duration of
+-- the supplied block.
+--
+-- The directory will have the prefix as "$prefixPath/$moduleName" but contain a generated random
+-- suffix to prevent interference between tests
+--
+-- The directory will be deleted if the block succeeds, but left behind if
+-- the block fails.
+--
+-- The 'prefix' argument should not contain directory delimeters.
+moduleWorkspace ::  ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error H.Failure
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: Log Text
+  => String
+  -> Eff (Reader Workspace : r) ()
+  -> Eff r ()
+moduleWorkspace prefix f = withFrozenCallStack $
+  workspace (prefix <> "-" <> callerModuleName) f
+
+-- | Compute the project base.  This will be the first parent directory that contains
+-- the `cabal.project` file.
+-- This should should point to the root directory of the Github project checkout.
+findCabalProjectDir :: ()
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error H.Failure
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: Log Text
+  => FilePath
+  -> Eff r FilePath
+findCabalProjectDir dir = do
+  atBase <- doesFileExist (dir </> "cabal.project")
+    & trap_ @IOException (pure False)
+  if atBase
+    then return dir
+    else do
+      let up = dir </> ".."
+      upExist <- doesDirectoryExist up
+        & trap_ @IOException (pure False)
+      if upExist
+        then findCabalProjectDir up
+        else failWith Nothing "Could not detect project base directory (containing cabal.project)"
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Data.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Data.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Data.hs
@@ -0,0 +1,15 @@
+module Effectful.Zoo.Hedgehog.Data
+  ( PackagePath(..),
+    ProjectRoot(..),
+    PropertyTest,
+    TestResult(..),
+    UnitTest,
+    Workspace(..),
+  ) where
+
+import Effectful.Zoo.Hedgehog.Data.PackagePath
+import Effectful.Zoo.Hedgehog.Data.ProjectRoot
+import Effectful.Zoo.Hedgehog.Data.PropertyTest
+import Effectful.Zoo.Hedgehog.Data.TestResult
+import Effectful.Zoo.Hedgehog.Data.UnitTest
+import Effectful.Zoo.Hedgehog.Data.Workspace
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Data/PackagePath.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/PackagePath.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/PackagePath.hs
@@ -0,0 +1,11 @@
+module Effectful.Zoo.Hedgehog.Data.PackagePath
+  ( PackagePath(..),
+  ) where
+
+import           GHC.Generics
+import           HaskellWorks.Prelude
+
+newtype PackagePath = PackagePath
+  { filePath :: FilePath
+  }
+  deriving stock (Eq, Generic, Show)
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Data/ProjectRoot.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/ProjectRoot.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/ProjectRoot.hs
@@ -0,0 +1,11 @@
+module Effectful.Zoo.Hedgehog.Data.ProjectRoot
+  ( ProjectRoot(..),
+  ) where
+
+import           GHC.Generics
+import           HaskellWorks.Prelude
+
+newtype ProjectRoot = ProjectRoot
+  { filePath :: FilePath
+  }
+  deriving stock (Eq, Generic, Show)
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Data/PropertyTest.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/PropertyTest.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/PropertyTest.hs
@@ -0,0 +1,8 @@
+module Effectful.Zoo.Hedgehog.Data.PropertyTest
+  ( PropertyTest,
+  ) where
+
+import Hedgehog
+import HaskellWorks.Prelude
+
+type PropertyTest = PropertyT IO ()
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Data/TestResult.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/TestResult.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/TestResult.hs
@@ -0,0 +1,11 @@
+module Effectful.Zoo.Hedgehog.Data.TestResult
+  ( TestResult(..),
+  ) where
+
+import HaskellWorks.Prelude
+import Hedgehog.Internal.Property qualified as H
+
+data TestResult a
+  = TestResult a
+  | TestFailure H.Failure
+  | TestError SomeException
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Data/UnitTest.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/UnitTest.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/UnitTest.hs
@@ -0,0 +1,8 @@
+module Effectful.Zoo.Hedgehog.Data.UnitTest
+  ( UnitTest,
+  ) where
+
+import Hedgehog
+import HaskellWorks.Prelude
+
+type UnitTest = TestT IO ()
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Data/Workspace.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/Workspace.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Data/Workspace.hs
@@ -0,0 +1,11 @@
+module Effectful.Zoo.Hedgehog.Data.Workspace
+  ( Workspace(..),
+  ) where
+
+import           GHC.Generics
+import           HaskellWorks.Prelude
+
+newtype Workspace = Workspace
+  { filePath :: FilePath
+  }
+  deriving stock (Eq, Generic, Show)
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Dynamic.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Dynamic.hs
deleted file mode 100644
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/Dynamic.hs
+++ /dev/null
@@ -1,67 +0,0 @@
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
-module Effectful.Zoo.Hedgehog.Dynamic
-  ( Hedgehog(..),
-    MonadTest(..),
-
-    runHedgehogIO,
-    runHedgehog,
-  ) where
-
-import Effectful
-import Effectful.Dispatch.Dynamic
-import Effectful.Error.Static
-import Effectful.Writer.Static.Local
-import Effectful.Zoo.Core
-import Effectful.Zoo.Hedgehog.MonadTestProxy
-import HaskellWorks.Prelude
-import Hedgehog (MonadTest(..))
-import Hedgehog qualified as H
-import Hedgehog.Internal.Property (Failure, Journal)
-import Hedgehog.Internal.Property qualified as H
-
-data Hedgehog :: Effect where
-  CatchAssertion :: HasCallStack
-    => m a
-    -> (H.Failure -> m a)
-    -> Hedgehog m a
-
-  LiftTest :: HasCallStack
-    => H.Test a
-    -> Hedgehog m a
-
-  ThrowAssertion :: HasCallStack
-    => H.Failure
-    -> Hedgehog m a
-
-type instance DispatchOf Hedgehog = Dynamic
-
-instance (r <: Hedgehog) => MonadTest (Eff r) where
-  liftTest t = send $ LiftTest t
-
-runHedgehogIO :: forall a. ()
-  => Eff
-      [ Hedgehog
-      , Error Failure
-      , Writer Journal
-      , IOE
-      ] a
-  -> IO (Either Failure a, Journal)
-runHedgehogIO f =
-  f
-    & runHedgehog
-    & runError @H.Failure
-    & runWriter @H.Journal
-    & fmap (first (first snd))
-    & runEff
-
-runHedgehog :: forall a r. ()
-  => r <: Error Failure
-  => r <: Writer Journal
-  => Eff (Hedgehog : r) a
-  -> Eff r a
-runHedgehog =
-  interpret $ \env -> \case
-    CatchAssertion f h -> localUnlift env SeqUnlift $ \unlift -> catchError (unlift f) (const (unlift . h))
-    LiftTest f -> liftTestProxy f
-    ThrowAssertion failure -> throwError failure
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Hedgehog.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Hedgehog.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Hedgehog.hs
@@ -0,0 +1,83 @@
+{- HLINT ignore "Eta reduce" -}
+
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Effectful.Zoo.Hedgehog.Effect.Hedgehog
+  ( Hedgehog,
+    HedgehogEnv(..),
+    runHedgehogProperty,
+    runHedgehogUnit,
+  ) where
+
+import Control.Concurrent.STM qualified as IO
+import Control.Monad
+import Control.Monad.Catch (MonadThrow(..))
+import Effectful
+import Effectful.Concurrent
+import Effectful.Concurrent.STM
+import Effectful.Dispatch.Static
+import Effectful.Zoo.Core
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Hedgehog.Api.MonadAssertion
+import Effectful.Zoo.Hedgehog.Data.TestResult
+import HaskellWorks.Prelude
+import Hedgehog (MonadTest(..))
+import Hedgehog qualified as H
+import Hedgehog.Internal.Property qualified as H
+
+-- | An effect for interacting with the filesystem.
+data Hedgehog :: Effect
+
+type instance DispatchOf Hedgehog = Static WithSideEffects
+newtype instance StaticRep Hedgehog = Hedgehog HedgehogEnv
+
+data HedgehogEnv
+  = PropertyEnv (TMVar (H.PropertyT IO ()))
+  | UnitTestEnv (TMVar (H.TestT IO ()))
+
+instance {-# OVERLAPS #-}
+    ( r <: Concurrent
+    , r <: Error H.Failure
+    , r <: Hedgehog
+    ) => MonadTest (Eff r) where
+  liftTest f = do
+    Hedgehog env <- getStaticRep
+    mvA <- newEmptyTMVarIO
+    case env of
+      PropertyEnv mvAction ->
+        atomically $ putTMVar mvAction (tryExceptAssertion (liftTest f) >>= liftIO . IO.atomically . IO.putTMVar mvA)
+      UnitTestEnv mvAction ->
+        atomically $ putTMVar mvAction (tryExceptAssertion (liftTest f) >>= liftIO . IO.atomically . IO.putTMVar mvA)
+    testResult <- atomically $ takeTMVar mvA
+    getTestResult testResult
+
+instance
+    ( r <: Error H.Failure
+    ) => MonadAssertion (Eff r) where
+  throwAssertion f = throw f
+  catchAssertion g h = g & trapIn h
+
+getTestResult :: ()
+  => r <: Error H.Failure
+  => TestResult a
+  -> Eff r a
+getTestResult = \case
+  TestResult a -> pure a
+  TestFailure f -> throw f
+  TestError e -> throwM e
+
+runHedgehogProperty :: ()
+  => r <: IOE
+  => TMVar (H.PropertyT IO ())
+  -> Eff (Hedgehog : r) a
+  -> Eff r a
+runHedgehogProperty tvAction =
+  evalStaticRep (Hedgehog (PropertyEnv tvAction))
+
+runHedgehogUnit :: ()
+  => r <: IOE
+  => TMVar (H.TestT IO ())
+  -> Eff (Hedgehog : r) a
+  -> Eff r a
+runHedgehogUnit tvAction =
+  evalStaticRep (Hedgehog (UnitTestEnv tvAction))
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/HedgehogGen.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/HedgehogGen.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/HedgehogGen.hs
@@ -0,0 +1,41 @@
+{- HLINT ignore "Eta reduce" -}
+
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Effectful.Zoo.Hedgehog.Effect.HedgehogGen
+  ( HedgehogGen,
+    HedgehogGenEnv(..),
+    askHedgehogGenEnv,
+    runHedgehogGenProperty,
+  ) where
+
+import Effectful
+import Effectful.Concurrent.STM
+import Effectful.Dispatch.Static
+import Effectful.Zoo.Core
+import HaskellWorks.Prelude
+import Hedgehog qualified as H
+
+-- | An effect for interacting with the filesystem.
+data HedgehogGen :: Effect
+
+type instance DispatchOf HedgehogGen = Static WithSideEffects
+newtype instance StaticRep HedgehogGen = HedgehogGen HedgehogGenEnv
+
+newtype HedgehogGenEnv
+  = HedgehogGenEnv (TMVar (H.PropertyT IO ()))
+
+askHedgehogGenEnv :: ()
+  => r <: HedgehogGen
+  => Eff r HedgehogGenEnv
+askHedgehogGenEnv = do
+  HedgehogGen env <- getStaticRep
+  pure env
+
+runHedgehogGenProperty :: ()
+  => r <: IOE
+  => TMVar (H.PropertyT IO ())
+  -> Eff (HedgehogGen : r) a
+  -> Eff r a
+runHedgehogGenProperty tvAction =
+  evalStaticRep (HedgehogGen (HedgehogGenEnv tvAction))
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Run.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Run.hs
new file mode 100644
--- /dev/null
+++ b/components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Run.hs
@@ -0,0 +1,156 @@
+{- HLINT ignore "Eta reduce" -}
+
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Effectful.Zoo.Hedgehog.Effect.Run
+  ( Hedgehog,
+    property,
+    unit,
+    forAll,
+  ) where
+
+import Control.Concurrent qualified as IO
+import Control.Concurrent.STM qualified as IO
+import Control.Exception qualified as IO
+import Control.Exception.Lifted qualified as CEL
+import Control.Monad
+import Effectful
+import Effectful.Concurrent
+import Effectful.Concurrent.STM
+import Effectful.Concurrent.STM qualified as CC
+import Effectful.Environment
+import Effectful.FileSystem
+import Effectful.Zoo.Core
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Hedgehog.Api.Failure
+import Effectful.Zoo.Hedgehog.Api.Journal
+import Effectful.Zoo.Hedgehog.Api.MonadAssertion
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
+import Effectful.Zoo.Hedgehog.Effect.HedgehogGen
+import Effectful.Zoo.Log.Data.LogMessage
+import Effectful.Zoo.Log.Dynamic
+import Effectful.Zoo.Resource
+import HaskellWorks.Control.Monad
+import HaskellWorks.Prelude
+import HaskellWorks.ToText
+import Hedgehog (Gen)
+import Hedgehog qualified as H
+import Hedgehog.Internal.Property qualified as H
+
+property :: ()
+  => Eff
+      [ Log Text
+      , HedgehogGen
+      , Hedgehog
+      , Error H.Failure
+      , Resource
+      , FileSystem
+      , Environment
+      , Concurrent
+      , IOE
+      ] ()
+  -> H.PropertyT IO ()
+property f = do
+  tvResult <- liftIO IO.newEmptyTMVarIO
+  tvAction <- liftIO IO.newEmptyTMVarIO
+  CEL.bracket
+    do  liftIO $ IO.forkFinally
+          do  f
+                & runLog @Text (ConcUnlift Persistent Unlimited) logTextToHedgehog
+                & runHedgehogGenProperty tvAction
+                & runHedgehogProperty tvAction
+                & runError @H.Failure
+                & runResource
+                & runFileSystem
+                & runEnvironment
+                & runConcurrent
+                & runEff
+          (liftIO . IO.atomically . IO.putTMVar tvResult)
+    do liftIO . IO.killThread
+    do \_ -> do
+        whileNothingM do
+          mActionOrResult <- liftIO $ IO.atomically do
+            mAction <- CC.tryTakeTMVar tvAction
+            case mAction of
+              Nothing -> do
+                mResult <- CC.tryTakeTMVar tvResult
+                case mResult of
+                  Nothing -> pure Nothing
+                  Just a -> pure $ Just $ Right a
+              Just action -> pure $ Just $ Left action
+
+          case mActionOrResult of
+            Nothing -> pure Nothing
+            Just (Left action) -> action >> pure Nothing
+            Just (Right (Left e)) -> liftIO $ IO.throwIO e
+            Just (Right (Right (Left (_, e)))) -> throwAssertion e
+            Just (Right (Right (Right a))) -> pure $ Just a
+
+logTextToHedgehog :: ()
+  => r <: Concurrent
+  => r <: Hedgehog
+  => r <: Error Failure
+  => CallStack
+  -> LogMessage Text
+  -> Eff r ()
+logTextToHedgehog callStack msg = do
+  let LogMessage severity text = msg
+  jotTextWithCallStack callStack $ toText severity <> " " <> text
+
+unit :: ()
+  => Eff
+      [ Log Text
+      , Hedgehog
+      , Error H.Failure
+      , Resource
+      , FileSystem
+      , Environment
+      , Concurrent
+      , IOE
+      ] ()
+  -> H.TestT IO ()
+unit f = do
+  tvResult <- liftIO IO.newEmptyTMVarIO
+  tvAction <- liftIO IO.newEmptyTMVarIO
+  CEL.bracket
+    do  liftIO $ IO.forkFinally
+          do  f & runLog @Text (ConcUnlift Persistent Unlimited) logTextToHedgehog
+                & runHedgehogUnit tvAction
+                & runError @H.Failure
+                & runResource
+                & runFileSystem
+                & runEnvironment
+                & runConcurrent
+                & runEff
+          (liftIO . IO.atomically . IO.putTMVar tvResult)
+    do liftIO . IO.killThread
+    do \_ -> do
+        whileNothingM do
+          mActionOrResult <- liftIO $ IO.atomically do
+            mAction <- CC.tryTakeTMVar tvAction
+            case mAction of
+              Nothing -> do
+                mResult <- CC.tryTakeTMVar tvResult
+                case mResult of
+                  Nothing -> pure Nothing
+                  Just a -> pure $ Just $ Right a
+              Just action -> pure $ Just $ Left action
+
+          case mActionOrResult of
+            Nothing -> pure Nothing
+            Just (Left action) -> action >> pure Nothing
+            Just (Right (Left e)) -> liftIO $ IO.throwIO e
+            Just (Right (Right (Left (_, e)))) -> throwAssertion e
+            Just (Right (Right (Right a))) -> pure $ Just a
+
+forAll :: ()
+  => r <: Concurrent
+  => r <: HedgehogGen
+  => Show a
+  => Gen a
+  -> Eff r a
+forAll gen = do
+  HedgehogGenEnv mvAction <- askHedgehogGenEnv
+  mvA <- newEmptyTMVarIO
+  atomically $ putTMVar mvAction (H.forAll gen >>= liftIO . IO.atomically . IO.putTMVar mvA)
+  atomically $ takeTMVar mvA
diff --git a/components/hedgehog/Effectful/Zoo/Hedgehog/MonadTestProxy.hs b/components/hedgehog/Effectful/Zoo/Hedgehog/MonadTestProxy.hs
deleted file mode 100644
--- a/components/hedgehog/Effectful/Zoo/Hedgehog/MonadTestProxy.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Effectful.Zoo.Hedgehog.MonadTestProxy
-  ( MonadTestProxy(..),
-  ) where
-
-import Control.Monad.Trans.Writer.Lazy qualified as MTL
-import Data.Functor.Identity
-import Effectful
-import Effectful.Error.Static
-import Effectful.Writer.Static.Local
-import Effectful.Zoo.Core
-import HaskellWorks.Prelude
-import Hedgehog (MonadTest(..))
-import Hedgehog.Internal.Property (Failure, Journal)
-import Hedgehog.Internal.Property qualified as H
-
-class Monad m => MonadTestProxy m where
-  liftTestProxy :: H.Test a -> m a
-
-instance MonadTestProxy (H.TestT IO) where
-  liftTestProxy = liftTest
-
-instance (r <: Error Failure, r <: Writer Journal) => MonadTestProxy (Eff r) where
-  liftTestProxy = \case
-    H.TestT m -> do
-      let (result, journal) = runIdentity $ MTL.runWriterT $ runExceptT m
-      tell journal
-      case result of
-        Left (H.Failure loc err diff) ->
-          throwError (H.Failure loc err diff)
-        Right a ->
-          pure a
diff --git a/components/rds-data-test/Effectful/Zoo/RdsData/Test/Cluster.hs b/components/rds-data-test/Effectful/Zoo/RdsData/Test/Cluster.hs
new file mode 100644
--- /dev/null
+++ b/components/rds-data-test/Effectful/Zoo/RdsData/Test/Cluster.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE FlexibleContexts    #-}
+{-# LANGUAGE NumericUnderscores  #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications    #-}
+
+{- HLINT ignore "Redundant id" -}
+{- HLINT ignore "Redundant pure" -}
+{- HLINT ignore "Use let" -}
+
+module Effectful.Zoo.RdsData.Test.Cluster
+  ( RdsClusterDetails(..),
+    createRdsDbCluster,
+    waitUntilRdsDbClusterAvailable,
+  ) where
+
+import Amazonka qualified as AWS
+import Amazonka.RDS qualified as AWS
+import Amazonka.SecretsManager qualified as AWS
+import Data.Aeson ((.=))
+import Data.Aeson qualified as J
+import Data.ByteString.Base64 qualified as B64
+import Data.ByteString.Lazy qualified as LBS
+import Data.Function
+import Data.Generics.Product.Any
+import Data.RdsData.Migration.Types (RdsClusterDetails (RdsClusterDetails))
+import Data.Text.Encoding qualified as T
+import Data.UUID qualified as UUID
+import Data.UUID.V4 qualified as UUID
+import Effectful
+import Effectful.Concurrent
+import Effectful.Zoo.Amazonka.Api.Send
+import Effectful.Zoo.Amazonka.Data.AwsError
+import Effectful.Zoo.Amazonka.Data.AwsLogEntry
+import Effectful.Zoo.Amazonka.Dynamic
+import Effectful.Zoo.Core
+import Effectful.Zoo.DataLog.Dynamic
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Hedgehog.Api.Assert
+import Effectful.Zoo.Hedgehog.Api.Failure
+import Effectful.Zoo.Hedgehog.Api.Journal
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
+import Effectful.Zoo.TestContainers.LocalStack
+import HaskellWorks.Control.Monad
+import HaskellWorks.Prelude
+import Lens.Micro
+
+createRdsDbCluster :: ()
+  => HasCallStack
+  => r <: Amazonka
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Hedgehog
+  => r <: IOE
+  => Text
+  -> IO Container
+  -> Eff r RdsClusterDetails
+createRdsDbCluster databaseName getContainer = withFrozenCallStack do
+  container <- liftIO getContainer
+  jotShowM_ $ getLocalStackEndpoint container
+  jotYamlM_ $ inspectContainer container
+  masterUsername <- pure "masterUsername"
+  masterPassword <- pure "masterPassword"
+
+  let dbClusterId = "my-cluster"
+
+  createDbClusterRequest <-
+    pure $
+      AWS.newCreateDBCluster dbClusterId "aurora-postgresql"
+        & the @"masterUsername" .~ Just masterUsername
+        & the @"masterUserPassword" .~ Just masterPassword
+        & the @"enableHttpEndpoint" .~ Just True
+        & the @"databaseName" .~ Just databaseName
+
+  createDbClusterResponse <-
+    sendAws createDbClusterRequest
+      & trapFail @AwsError
+      & jotShowDataLog @AwsLogEntry
+
+
+  -- sendAws :: forall a r. ()
+  --   => HasCallStack
+  --   => AwsRequest a
+  --   => r <: Amazonka
+  --   => r <: DataLog AwsLogEntry
+  --   => r <: Error AwsError
+  --   => r <: IOE
+  --   => Typeable (AwsResponse a)
+  --   => Typeable a
+  --   => a
+  --   -> Eff r (AwsResponse a)
+
+  -- jotShowDataLog :: forall i a r. ()
+  --   => HasCallStack
+  --   => Show i
+  --   => r <: Concurrent
+  --   => r <: Error Failure
+  --   => r <: Hedgehog
+  --   => Eff (DataLog i : r) a
+  --   -> Eff r a
+
+  let secretName = "my-aurora-cluster"
+
+  secretString <-
+    jotYaml $
+      J.object
+        [ "engine" .= id @Text "aurora-postgresql"
+        , "username" .= id @Text masterUsername
+        , "password" .= id @Text masterPassword
+        , "host" .= id @Text "localhost"
+        , "dbname" .= id @Text databaseName
+        , "port" .= id @Text "4510"
+        ]
+
+  uuid <- liftIO UUID.nextRandom
+
+  let clientRequestToken = T.encodeUtf8 $ UUID.toText uuid
+
+  let secretStringText = T.decodeUtf8 $ LBS.toStrict $ J.encode secretString
+
+  createSecretReq <-
+    pure $
+      AWS.newCreateSecret secretName
+        & the @"secretString" ?~ AWS.Sensitive secretStringText
+        & the @"clientRequestToken" ?~ T.decodeUtf8 (B64.encode clientRequestToken)
+
+  createSecetResp <-
+    sendAws createSecretReq
+      & trapFail
+      & jotShowDataLog @AwsLogEntry
+
+  createDbInstanceReq <-
+    pure $
+      AWS.newCreateDBInstance dbClusterId "my-db-instance" "db.t3.medium"
+        & the @"engine" .~ "aurora-postgresql"
+
+  _dbInstanceIdResp <-
+    jotShowM $
+      sendAws createDbInstanceReq
+        & trapFail
+        & jotShowDataLog @AwsLogEntry
+
+  pure (RdsClusterDetails createDbClusterResponse createSecetResp)
+
+waitUntilRdsDbClusterAvailable :: ()
+  => HasCallStack
+  => r <: Amazonka
+  => r <: Concurrent
+  => r <: DataLog AwsLogEntry
+  => r <: Error AWS.Error
+  => r <: IOE
+  => Text
+  -> Eff r ()
+waitUntilRdsDbClusterAvailable dbClusterArn =
+  withFrozenCallStack do
+    repeatNWhileM_ 120 $ \_ -> do
+      result <- sendAws $
+        AWS.newDescribeDBClusters
+          & the @"dbClusterIdentifier" .~ Just dbClusterArn
+
+      let mStatus = result ^? the @"dbClusters" . _Just . each . the @"status" . _Just
+
+      if mStatus == Just "available"
+        then threadDelay 1_000_000 >> pure False
+        else threadDelay 1_000_000 >> pure True
diff --git a/components/rds-data-test/Effectful/Zoo/RdsData/Test/Env.hs b/components/rds-data-test/Effectful/Zoo/RdsData/Test/Env.hs
new file mode 100644
--- /dev/null
+++ b/components/rds-data-test/Effectful/Zoo/RdsData/Test/Env.hs
@@ -0,0 +1,109 @@
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE FlexibleContexts    #-}
+{-# LANGUAGE FlexibleInstances   #-}
+{-# LANGUAGE GADTs               #-}
+{-# LANGUAGE PolyKinds           #-}
+{-# LANGUAGE RankNTypes          #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications    #-}
+{-# LANGUAGE TypeOperators       #-}
+
+{- HLINT ignore "Redundant pure" -}
+{- HLINT ignore "Use let" -}
+
+module Effectful.Zoo.RdsData.Test.Env
+  ( AwsResourceArn(..),
+    AwsSecretArn(..),
+    runLocalTestEnv,
+    runTestEnv,
+    runReaderFromEnvOrFail,
+    runReaderStatementContextFromClusterDetails,
+  ) where
+
+import Amazonka qualified as AWS
+import Data.Generics.Product.Any
+import Data.RdsData.Aws
+import Data.RdsData.Migration.Types
+import Effectful
+import Effectful.Concurrent
+import Effectful.Zoo.Amazonka.Api.Run
+import Effectful.Zoo.Core
+import Effectful.Zoo.Environment
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.Hedgehog.Api
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
+import Effectful.Zoo.Reader.Static
+import Effectful.Zoo.TestContainers.LocalStack
+import HaskellWorks.Prelude
+import Lens.Micro
+
+runTestEnv :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error Failure
+  => r <: Hedgehog
+  => r <: IOE
+  => Eff
+        ( Reader AWS.Env
+        : Reader AwsResourceArn
+        : Reader AwsSecretArn
+        : r)
+      a
+  -> Eff r a
+runTestEnv f =
+  withFrozenCallStack $ f
+    & runReaderAwsEnvDiscover
+    & runReaderFromEnvOrFail AwsResourceArn "AURORA_RESOURCE_ARN"
+    & runReaderFromEnvOrFail AwsSecretArn "AURORA_SECRET_ARN"
+
+runLocalTestEnv :: ()
+  => HasCallStack
+  => r <: IOE
+  => IO Container
+  -> Eff
+        ( Reader AWS.Env
+        : r)
+      a
+  -> Eff r a
+runLocalTestEnv getContainer f =
+  withFrozenCallStack $ f
+    & runReaderLocalAwsEnvDiscover getContainer
+
+runReaderFromEnvOrFail :: forall i r a. ()
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error Failure
+  => r <: Hedgehog
+  => (Text -> i)
+  -> Text
+  -> Eff (Reader i ': r) a
+  -> Eff r a
+runReaderFromEnvOrFail f envVar action = do
+  env <- lookupEnv envVar
+    & trapFail
+
+  runReader (f env) action
+
+runReaderStatementContextFromClusterDetails :: ()
+  => r <: Concurrent
+  => r <: Error Failure
+  => r <: Hedgehog
+  => RdsClusterDetails
+  -> Eff (Reader StatementContext : r) a
+  -> Eff r a
+runReaderStatementContextFromClusterDetails details f = do
+  resourceArn <- (details ^. the @"createDbClusterResponse" . the @"dbCluster" . _Just . the @"dbClusterArn")
+    & onNothingFail
+
+  secretArn <- (details ^. the @"createSecretResponse" ^. the @"arn")
+    & onNothingFail
+
+  mDatabase <- pure $ (details ^? the @"createDbClusterResponse" . the @"dbCluster" . _Just . the @"databaseName" . _Just)
+    <&> Database
+
+  statementContext <- pure $ newStatementContext (AwsResourceArn resourceArn) (AwsSecretArn secretArn)
+    & the @"database" .~ mDatabase
+
+
+  f & runReader statementContext
diff --git a/components/rds-data-test/Effectful/Zoo/RdsData/Test/Workspace.hs b/components/rds-data-test/Effectful/Zoo/RdsData/Test/Workspace.hs
new file mode 100644
--- /dev/null
+++ b/components/rds-data-test/Effectful/Zoo/RdsData/Test/Workspace.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE DataKinds         #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeOperators     #-}
+
+module Effectful.Zoo.RdsData.Test.Workspace
+  ( localWorkspace,
+  ) where
+
+import Data.Text qualified as T
+import Effectful
+import Effectful.Concurrent
+import Effectful.Zoo.Core
+import Effectful.Zoo.Environment
+import Effectful.Zoo.Error.Static
+import Effectful.Zoo.FileSystem
+import Effectful.Zoo.Hedgehog.Api
+import Effectful.Zoo.Hedgehog.Api.Workspace
+import Effectful.Zoo.Hedgehog.Effect.Hedgehog
+import Effectful.Zoo.Log.Dynamic
+import Effectful.Zoo.Reader.Static
+import HaskellWorks.Prelude
+
+localWorkspace :: ()
+  => HasCallStack
+  => r <: Concurrent
+  => r <: Environment
+  => r <: Error Failure
+  => r <: FileSystem
+  => r <: Hedgehog
+  => r <: Log Text
+  => Text
+  -> Eff
+        ( Reader Workspace
+        : Reader ProjectRoot
+        : Reader PackagePath
+        : r)
+        ()
+  -> Eff r ()
+localWorkspace prefix f =
+  withFrozenCallStack $ do
+    cabalProjectDir <- findCabalProjectDir "."
+
+    f & moduleWorkspace (T.unpack prefix)
+      & runReader (ProjectRoot cabalProjectDir)
+      & runReader (PackagePath "rds-data")
diff --git a/components/rds-data/Effectful/Zoo/RdsData/Core.hs b/components/rds-data/Effectful/Zoo/RdsData/Core.hs
--- a/components/rds-data/Effectful/Zoo/RdsData/Core.hs
+++ b/components/rds-data/Effectful/Zoo/RdsData/Core.hs
@@ -24,8 +24,8 @@
 import Effectful.Zoo.Amazonka.Data
 import Effectful.Zoo.Amazonka.Dynamic
 import Effectful.Zoo.Core
-import Effectful.Zoo.Core.Error.Static
 import Effectful.Zoo.DataLog.Dynamic
+import Effectful.Zoo.Error.Static
 import Effectful.Zoo.Log.Api
 import Effectful.Zoo.Log.Dynamic
 import Effectful.Zoo.RdsData.Errors
diff --git a/components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack.hs b/components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack.hs
new file mode 100644
--- /dev/null
+++ b/components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack.hs
@@ -0,0 +1,139 @@
+{- HLINT ignore "Use camelCase" -}
+
+module Effectful.Zoo.TestContainers.LocalStack (
+    LocalStackEndpoint (..),
+    TC.Container,
+    setupContainers,
+    setupContainers',
+    waitForLocalStack,
+
+    runReaderLocalAwsEnvDiscover,
+    getLocalStackEndpoint,
+    inspectContainer,
+) where
+
+import Amazonka qualified as AWS
+import Amazonka.Auth qualified as AWS
+import Control.Concurrent (threadDelay)
+import Control.Concurrent qualified as IO
+import Control.Exception  (try)
+import Control.Exception qualified as E
+import Control.Monad.IO.Class
+import Data.Aeson qualified as J
+import Data.ByteString.Lazy qualified as LBS
+import Data.Function
+import Data.Generics.Product.Any
+import Data.Text qualified as T
+import Data.Time.Clock.POSIX (getPOSIXTime)
+import Effectful
+import Effectful.Zoo.Core
+import Effectful.Zoo.Reader.Static
+import Effectful.Zoo.TestContainers.LocalStack.Types (LocalStackEndpoint (LocalStackEndpoint))
+import Effectful.Zoo.TestContainers.LocalStack.Types qualified as Z
+import HaskellWorks.Prelude
+import Lens.Micro
+import Network.HTTP.Conduit (HttpException, simpleHttp)
+import System.Environment qualified as IO
+import System.IO qualified as IO
+import TestContainers.Monad qualified as TC
+import TestContainers.Tasty qualified as TC
+
+-- | Sets up and runs the containers required for this test suite.
+setupContainers ::
+    () =>
+    (TC.MonadDocker m) =>
+    m TC.Container
+setupContainers = setupContainers' "localstack/localstack-pro:latest"
+
+-- | Sets up and runs the containers required for this test suite.
+setupContainers' ::
+    () =>
+    (TC.MonadDocker m) =>
+    Text ->
+    m TC.Container
+setupContainers' dockerTag = do
+    authToken <- liftIO $ IO.lookupEnv "LOCALSTACK_AUTH_TOKEN"
+    -- Launch the container based on the postgres image.
+    localstackContainer <-
+        TC.run $
+            TC.containerRequest (TC.fromTag dockerTag)
+                & TC.setEnv [("LOCALSTACK_AUTH_TOKEN", maybe "" T.pack authToken)]
+                -- Expose the port 4566 from within the container. The respective port
+                -- on the host machine can be looked up using `containerPort` (see below).
+                & TC.setExpose
+                    ( mconcat
+                        [ [4566]
+                        ]
+                    )
+                -- Wait until the container is ready to accept requests. `run` blocks until
+                -- readiness can be established.
+                & TC.setWaitingFor (TC.waitUntilMappedPortReachable 4566)
+
+    -- Look up the corresponding port on the host machine for the exposed port 4566.
+    let localStackPort = TC.containerPort localstackContainer 4566
+
+    liftIO $ waitForLocalStack "localhost" localStackPort 100
+
+    pure localstackContainer
+
+waitForLocalStack :: String -> Int -> Int -> IO ()
+waitForLocalStack host port timeout = do
+    startTime <- getPOSIXTime
+    let url = "http://" <> host <> ":" <> show port
+    checkLoop startTime url
+  where
+    checkLoop startTime url = do
+        result <- try $ simpleHttp url :: IO (Either HttpException LBS.ByteString)
+        case result of
+            Right _ -> do
+                IO.threadDelay 1_000_000
+                IO.putStrLn ""
+            Left e -> do
+                currentTime <- getPOSIXTime
+                let elapsedTime = currentTime - startTime
+                when (elapsedTime < fromIntegral timeout) $ do
+                    threadDelay 500_000
+                    checkLoop startTime url
+                when (elapsedTime >= fromIntegral timeout) $ do
+                    IO.putStrLn "Timeout reached. LocalStack is not ready."
+                    E.throw e
+
+runReaderLocalAwsEnvDiscover :: forall a r. ()
+  => r <: IOE
+  => IO TC.Container
+  -> Eff (Reader AWS.Env : r) a
+  -> Eff r a
+runReaderLocalAwsEnvDiscover mk f = do
+  container <- liftIO mk
+  ep <- getLocalStackEndpoint container
+
+  logger' <- liftIO $ AWS.newLogger AWS.Debug IO.stdout
+
+  let creds = AWS.fromKeys (AWS.AccessKey "test") (AWS.SecretKey "test")
+
+  credEnv <- liftIO $ AWS.newEnv (AWS.runCredentialChain [pure . creds])
+
+  awsEnv <- pure $
+    credEnv
+      & the @"logger" .~ logger'
+      & the @"overrides" %~ (. AWS.setEndpoint False "localhost" ep.port)
+
+  runReader awsEnv f
+
+getLocalStackEndpoint :: ()
+  => TC.Container
+  -> Eff r LocalStackEndpoint
+getLocalStackEndpoint container = do
+  let localStackPort = TC.containerPort container 4566
+
+  pure Z.LocalStackEndpoint
+    { Z.host = "0.0.0.0"
+    , Z.port = localStackPort
+    }
+
+inspectContainer :: ()
+  => r <: IOE
+  => TC.Container
+  -> Eff r J.Value
+inspectContainer container =
+  liftIO $ TC.runTestContainer TC.defaultDockerConfig $ TC.inspect container
diff --git a/components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack/Types.hs b/components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack/Types.hs
new file mode 100644
--- /dev/null
+++ b/components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack/Types.hs
@@ -0,0 +1,10 @@
+module Effectful.Zoo.TestContainers.LocalStack.Types
+  ( LocalStackEndpoint(..)
+  ) where
+
+import HaskellWorks.Prelude
+
+data LocalStackEndpoint = LocalStackEndpoint
+  { host :: String
+  , port :: Int
+  } deriving stock (Eq, Show, Generic)
diff --git a/effectful-zoo.cabal b/effectful-zoo.cabal
--- a/effectful-zoo.cabal
+++ b/effectful-zoo.cabal
@@ -1,7 +1,7 @@
 cabal-version: 3.4
 
 name:                   effectful-zoo
-version:                0.0.2.0
+version:                0.0.3.0
 synopsis:               Effectful effects for testing
 description:            See https://hackage.haskell.org/package/effectful-zoo/docs/effectful-zoo.html
 category:               Development
@@ -16,6 +16,11 @@
 extra-doc-files:        README.md
                         CHANGELOG.md
 
+flag dev
+  description: Enable development mode
+  manual: True
+  default: False
+
 source-repository head
   type: git
   location: https://github.com/haskell-works/effectful-zoo
@@ -26,31 +31,49 @@
 common aeson-pretty               { build-depends: aeson-pretty               >= 0.8.10     && < 1      }
 common amazonka                   { build-depends: amazonka                   >= 2          && < 3      }
 common amazonka-core              { build-depends: amazonka-core              >= 2          && < 3      }
+common amazonka-rds               { build-depends: amazonka-rds               >= 2          && < 3      }
 common amazonka-rds-data          { build-depends: amazonka-rds-data          >= 2          && < 3      }
+common amazonka-secretsmanager    { build-depends: amazonka-secretsmanager    >= 2          && < 3      }
+common base64-bytestring          { build-depends: base64-bytestring          >= 1.2.1      && < 2      }
+common binary                     { build-depends: binary                     >= 0.8.9      && < 0.9    }
 common blockfrost-api             { build-depends: blockfrost-api             >= 0.11       && < 0.12   }
 common blockfrost-client          { build-depends: blockfrost-client          >= 0.8        && < 0.9    }
 common bytestring                 { build-depends: bytestring                 >= 0.11       && < 1      }
+common directory                  { build-depends: directory                  >= 1.2        && < 2      }
 common effectful                  { build-depends: effectful                  >= 2.5        && < 3      }
 common effectful-core             { build-depends: effectful-core             >= 2.5        && < 3      }
 common effectful-plugin           { build-depends: effectful-plugin           >= 1.1.0.4    && < 2      }
+common exceptions                 { build-depends: exceptions                 >= 0.10       && < 0.11   }
+common filepath                   { build-depends: filepath                   >= 1.4        && < 2      }
 common generic-lens               { build-depends: generic-lens               >= 2.2.2      && < 3      }
 common hedgehog                   { build-depends: hedgehog                   >= 1.5        && < 2      }
+common http-conduit               { build-depends: http-conduit               >= 2.3.9.1    && < 3      }
 common HUnit                      { build-depends: HUnit                      >= 1.6.2      && < 2      }
-common hw-prelude                 { build-depends: hw-prelude                 >= 0.0.4.2    && < 0.1    }
+common hw-prelude                 { build-depends: hw-prelude                 >= 0.0.4.4    && < 0.1    }
+common lifted-base                { build-depends: lifted-base                >= 0.2.3.12   && < 0.3    }
 common microlens                  { build-depends: microlens                  >= 0.4.13.1   && < 0.5    }
+common process                    { build-depends: process                    >= 1.6.19     && < 2      }
 common rds-data-codecs            { build-depends: rds-data:codecs            >= 0.1.1.1    && < 0.2    }
 common resourcet                  { build-depends: resourcet                  >= 1.3        && < 2      }
 common resourcet-effectful        { build-depends: resourcet-effectful        >= 1.0.1      && < 2      }
-common tasty                      { build-depends: tasty                      >= 1.4.3      && < 1.5    }
+common stm                        { build-depends: stm                        >= 2.5.1      && < 3      }
+common tasty                      { build-depends: tasty                      >= 1.5        && < 2      }
+common tasty-discover             { build-depends: tasty-discover             >= 5          && < 6      }
 common tasty-hedgehog             { build-depends: tasty-hedgehog             >= 1.1.0.0    && < 1.5    }
+common temporary                  { build-depends: temporary                  >= 1.3        && < 2      }
+common testcontainers             { build-depends: testcontainers             >= 0.5        && < 0.6    }
 common text                       { build-depends: text                       >= 2          && < 3      }
 common time                       { build-depends: time                       >= 1.12       && < 2      }
 common transformers               { build-depends: transformers               >= 0.5.6.2    && < 0.7    }
+common ulid                       { build-depends: ulid                       >= 0.3.2      && < 0.4    }
+common uuid                       { build-depends: uuid                       >= 1.3.16     && < 2      }
 common yaml                       { build-depends: yaml                       >= 0.11.11.2  && < 1      }
 
-common effectful-zoo-amazonka     { build-depends: effectful-zoo:amazonka                               }
-common effectful-zoo-core         { build-depends: effectful-zoo:core                                   }
-common effectful-zoo-hedgehog     { build-depends: effectful-zoo:hedgehog                               }
+common effectful-zoo-amazonka                   { build-depends: effectful-zoo:amazonka                   }
+common effectful-zoo-core                       { build-depends: effectful-zoo:core                       }
+common effectful-zoo-hedgehog                   { build-depends: effectful-zoo:hedgehog                   }
+common effectful-zoo-rds-data                   { build-depends: effectful-zoo:rds-data                   }
+common effectful-zoo-testcontainers-localstack  { build-depends: effectful-zoo:testcontainers-localstack  }
 
 common project-config
   default-extensions:   AllowAmbiguousTypes
@@ -97,21 +120,28 @@
                         -Wredundant-constraints
                         -Wunused-packages
   default-language:     GHC2021
+  if (flag(dev))
+    ghc-options:        -Werror
 
 library core
   import:               base, project-config,
                         aeson,
                         bytestring,
+                        directory,
                         effectful-core,
                         effectful-core,
                         effectful-plugin,
                         effectful,
                         hw-prelude,
+                        process,
+                        resourcet-effectful,
                         text,
+                        temporary,
                         time,
                         yaml,
   visibility:           public
-  exposed-modules:      Effectful.Zoo.Console.Data.Writer
+  exposed-modules:      Effectful.Zoo.Aeson
+                        Effectful.Zoo.Console.Data.Writer
                         Effectful.Zoo.Console.Dynamic
                         Effectful.Zoo.Console.Dynamic.Api
                         Effectful.Zoo.Console.Dynamic.Effect
@@ -119,17 +149,19 @@
                         Effectful.Zoo.Console.Effect
                         Effectful.Zoo.Console.Static
                         Effectful.Zoo.Core
-                        Effectful.Zoo.Core.Error.Dynamic
-                        Effectful.Zoo.Core.Error.Static
-                        Effectful.Zoo.Core.Exception
-                        Effectful.Zoo.Core.Function
-                        Effectful.Zoo.Core.Prim
                         Effectful.Zoo.DataLog.Api
                         Effectful.Zoo.DataLog.Data.DataLogger
                         Effectful.Zoo.DataLog.Data.LogEntry
                         Effectful.Zoo.DataLog.Dynamic
                         Effectful.Zoo.DataLog.Static
+                        Effectful.Zoo.Environment
+                        Effectful.Zoo.Error.Dynamic
+                        Effectful.Zoo.Error.Static
+                        Effectful.Zoo.Errors.EnvironmentVariableInvalid
+                        Effectful.Zoo.Errors.EnvironmentVariableMissing
+                        Effectful.Zoo.Exception
                         Effectful.Zoo.FileSystem
+                        Effectful.Zoo.Function
                         Effectful.Zoo.Lazy.Dynamic
                         Effectful.Zoo.Log.Api
                         Effectful.Zoo.Log.Api.Generic
@@ -142,6 +174,10 @@
                         Effectful.Zoo.Log.Data.Severity
                         Effectful.Zoo.Log.Dynamic
                         Effectful.Zoo.Log.Static
+                        Effectful.Zoo.Prim
+                        Effectful.Zoo.Process
+                        Effectful.Zoo.Reader.Static
+                        Effectful.Zoo.Resource
                         Effectful.Zoo.Unsafe
   ghc-options:          -fplugin=Effectful.Plugin
   hs-source-dirs:       components/core
@@ -220,32 +256,91 @@
   ghc-options:          -fplugin=Effectful.Plugin
   hs-source-dirs:       components/rds-data
 
-library hedgehog
+library rds-data-test
   import:               base, project-config,
                         aeson,
+                        amazonka,
+                        amazonka-rds,
+                        amazonka-secretsmanager,
+                        base64-bytestring,
+                        bytestring,
+                        effectful,
+                        effectful-core,
+                        effectful-plugin,
+                        effectful-zoo-amazonka,
+                        effectful-zoo-core,
+                        effectful-zoo-hedgehog,
+                        effectful-zoo-testcontainers-localstack,
+                        generic-lens,
+                        hw-prelude,
+                        microlens,
+                        rds-data-codecs,
+                        text,
+                        uuid,
+  visibility:           public
+  exposed-modules:      Effectful.Zoo.RdsData.Test.Cluster
+                        Effectful.Zoo.RdsData.Test.Env
+                        Effectful.Zoo.RdsData.Test.Workspace
+  ghc-options:          -fplugin=Effectful.Plugin
+  hs-source-dirs:       components/rds-data-test
+
+library hedgehog
+  import:               base, project-config,
                         aeson-pretty,
+                        aeson,
+                        binary,
                         bytestring,
                         effectful-core,
                         effectful-plugin,
                         effectful-zoo-core,
+                        effectful,
+                        exceptions,
+                        filepath,
                         hedgehog,
                         hw-prelude,
+                        lifted-base,
+                        resourcet,
+                        stm,
+                        tasty-discover,
                         tasty-hedgehog,
                         tasty,
                         text,
+                        time,
                         transformers,
+                        ulid,
                         yaml,
   visibility:           public
   exposed-modules:      Effectful.Zoo.Hedgehog
                         Effectful.Zoo.Hedgehog.Api
                         Effectful.Zoo.Hedgehog.Api.Assert
-                        Effectful.Zoo.Hedgehog.Api.Classify
-                        Effectful.Zoo.Hedgehog.Api.Eval
                         Effectful.Zoo.Hedgehog.Api.Failure
+                        Effectful.Zoo.Hedgehog.Api.Gen
+                        Effectful.Zoo.Hedgehog.Api.Gen.Time
+                        Effectful.Zoo.Hedgehog.Api.Gen.Ulid
+                        Effectful.Zoo.Hedgehog.Api.Hedgehog
+                        Effectful.Zoo.Hedgehog.Api.Internal.Cabal
+                        Effectful.Zoo.Hedgehog.Api.Internal.Cabal.Types
+                        Effectful.Zoo.Hedgehog.Api.Internal.FilePath
+                        Effectful.Zoo.Hedgehog.Api.Internal.OS
                         Effectful.Zoo.Hedgehog.Api.Journal
-                        Effectful.Zoo.Hedgehog.Api.Run
-                        Effectful.Zoo.Hedgehog.Dynamic
-                        Effectful.Zoo.Hedgehog.MonadTestProxy
+                        Effectful.Zoo.Hedgehog.Api.MonadAssertion
+                        Effectful.Zoo.Hedgehog.Api.Process
+                        Effectful.Zoo.Hedgehog.Api.Process.Internal
+                        Effectful.Zoo.Hedgehog.Api.Range
+                        Effectful.Zoo.Hedgehog.Api.Stack
+                        Effectful.Zoo.Hedgehog.Api.Tasty
+                        Effectful.Zoo.Hedgehog.Api.Tasty.Orphans
+                        Effectful.Zoo.Hedgehog.Api.Workspace
+                        Effectful.Zoo.Hedgehog.Data
+                        Effectful.Zoo.Hedgehog.Data.PackagePath
+                        Effectful.Zoo.Hedgehog.Data.ProjectRoot
+                        Effectful.Zoo.Hedgehog.Data.PropertyTest
+                        Effectful.Zoo.Hedgehog.Data.TestResult
+                        Effectful.Zoo.Hedgehog.Data.UnitTest
+                        Effectful.Zoo.Hedgehog.Data.Workspace
+                        Effectful.Zoo.Hedgehog.Effect.Hedgehog
+                        Effectful.Zoo.Hedgehog.Effect.HedgehogGen
+                        Effectful.Zoo.Hedgehog.Effect.Run
   ghc-options:          -fplugin=Effectful.Plugin
   hs-source-dirs:       components/hedgehog
 
@@ -258,16 +353,38 @@
   exposed-modules:      Effectful.Zoo.HUnit
   hs-source-dirs:       components/hunit
 
-test-suite effectful-zoo-test
-  import:               project-config,
+library testcontainers-localstack
+  import:               base, project-config,
+                        aeson,
+                        amazonka,
+                        bytestring,
                         effectful-core,
+                        effectful-plugin,
                         effectful-zoo-core,
+                        generic-lens,
+                        http-conduit,
+                        hw-prelude,
+                        microlens,
+                        testcontainers,
+                        text,
+                        time,
+  visibility:           public
+  exposed-modules:      Effectful.Zoo.TestContainers.LocalStack
+                        Effectful.Zoo.TestContainers.LocalStack.Types
+  ghc-options:          -fplugin=Effectful.Plugin
+  hs-source-dirs:       components/testcontainers-localstack
+
+test-suite effectful-zoo-test
+  import:               base, project-config,
                         effectful-zoo-hedgehog,
+                        hedgehog,
                         hw-prelude,
                         tasty,
+                        tasty-discover,
   type:                 exitcode-stdio-1.0
   main-is:              Main.hs
-  other-modules:        Effectful.Zoo.Hedgehog.Test.HedgehogTest
+  other-modules:        Effectful.Zoo.Hedgehog.Test.PropertySpec
+  build-tool-depends:   tasty-discover:tasty-discover
   hs-source-dirs:       components/hedgehog-test
   ghc-options:          -threaded
                         -rtsopts
