diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# 1.0.0
+
+First dhrun version.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2018 Valentin Reis
+
+The cabal subdirectory contains code licensed under other terms. This license
+applies for the rest of this repository. Please seefile cabal/LICENSE for
+details.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,138 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+{-|
+Module      : dhall-exec.hs
+Description : dhall-exec main app file.
+Copyright   : (c) Valentin Reis, 2018
+License     : MIT
+Maintainer  : fre@freux.fr
+-}
+
+module Main
+  ( main
+  )
+where
+
+import           Protolude
+
+import qualified Prelude
+                   ( print )
+import           Dhrun.Types.Cfg                                   as DI
+import           Dhrun.Run                                         as DR
+import           Options.Applicative                               as OA
+import           Dhall
+import           System.FilePath.Posix
+import           System.Directory
+import           GHC.IO.Encoding
+import qualified System.IO                                         as SIO
+import qualified Data.ByteString                                   as B
+                   ( getContents )
+import           Text.Editor
+
+main :: IO ()
+main = do
+  GHC.IO.Encoding.setLocaleEncoding SIO.utf8
+  join . customExecParser (prefs showHelpOnError) $ info
+    (helper <*> opts)
+    (fullDesc <> header "dhrun" <> progDesc
+      (  "dhall-configured concurrent process execution"
+      <> " with streaming assertion monitoring"
+      )
+    )
+
+data MainCfg = MainCfg
+  { inputfile    :: Maybe Text
+  , stdinType    :: SourceType
+  , verbosity    :: Verbosity
+  , edit :: Bool
+}
+
+commonParser :: Parser MainCfg
+commonParser =
+  MainCfg
+    <$> optional
+          (strArgument
+            (  metavar "INPUT"
+            <> help
+                 "Input configuration with .yml/.yaml/.dh/.dhall extension. Leave void for stdin (dhall) input."
+            )
+          )
+    <*> flag
+          Dhall
+          Yaml
+          (long "yaml" <> short 'y' <> help
+            "Assume stdin to be yaml instead of dhall."
+          )
+    <*> flag Normal
+             Verbose
+             (long "verbose" <> short 'v' <> help "Enable verbose mode.")
+    <*> flag
+          False
+          True
+          (long "edit" <> short 'e' <> help "Edit yaml in $EDITOR before run.")
+
+opts :: Parser (IO ())
+opts =
+  hsubparser
+    $  command
+         "run"
+         (info (run <$> commonParser) $ progDesc "Run a dhrun specification.")
+    <> command
+         "print"
+         ( info (printY <$> commonParser)
+         $ progDesc "Print a dhrun specification."
+         )
+    <> help "Type of operation to run."
+
+
+data SourceType = Dhall | Yaml deriving (Eq)
+data FinallySource = NoExt | FinallyFile SourceType Text | FinallyStdin SourceType
+ext :: SourceType -> Maybe Text -> FinallySource
+ext _ (Just fn) | xt `elem` [".dh", ".dhall"] = FinallyFile Dhall fn
+                | xt `elem` [".yml", ".yaml"] = FinallyFile Yaml fn
+                | otherwise                   = NoExt
+  where xt = takeExtension $ toS fn
+ext st Nothing = FinallyStdin st
+
+load :: MainCfg -> IO Cfg
+load MainCfg {..} =
+  (if edit then editing else return)
+    =<< overrideV
+    <$> case ext stdinType inputfile of
+          (FinallyFile Dhall filename) ->
+            (if v then detailed else identity)
+              $   inputCfg
+              =<< toS
+              <$> makeAbsolute (toS filename)
+          (FinallyFile Yaml filename) ->
+            decodeCfgFile =<< toS <$> makeAbsolute (toS filename)
+          (FinallyStdin Yaml) -> B.getContents <&> decodeCfg >>= \case
+            Left  e   -> Prelude.print e >> die "yaml parsing exception."
+            Right cfg -> return cfg
+          (FinallyStdin Dhall) -> B.getContents >>= inputCfg . toS
+          NoExt                -> die
+            (  "couldn't figure out extension for input file. "
+            <> "Please use something in {.yml,.yaml,.dh,.dhall} ."
+            )
+ where
+  v = verbosity == Verbose
+  overrideV x = x
+    { DI.verbosity = if (DI.verbosity x == Verbose) || v
+                       then Verbose
+                       else Normal
+    }
+
+editing :: Cfg -> IO Cfg
+editing c = runUserEditorDWIM yt (encodeCfg c) <&> decodeCfg >>= \case
+  Left  e   -> Prelude.print e >> die "yaml parsing exception."
+  Right cfg -> return cfg
+  where yt = mkTemplate "yaml"
+
+run :: MainCfg -> IO ()
+run c = load c >>= DR.runDhrun
+
+printY :: MainCfg -> IO ()
+printY c = load c >>= putText . toS . encodeCfg
diff --git a/dhrun.cabal b/dhrun.cabal
new file mode 100644
--- /dev/null
+++ b/dhrun.cabal
@@ -0,0 +1,123 @@
+cabal-version: 2.0
+-- * * * * * * * * * * * * WARNING * * * * * * * * * * * *
+-- This file has been AUTO-GENERATED by dhall-to-cabal.
+--
+-- Do not edit it by hand, because your changes will be over-written!
+--
+-- Instead, edit the source Dhall file, namely
+-- 'dhrun.dhall', and re-generate this file by running
+-- 'dhall-to-cabal -- dhrun.dhall'.
+-- * * * * * * * * * * * * WARNING * * * * * * * * * * * *
+name: dhrun
+version: 1.0.1
+license: MIT
+license-file: LICENSE
+maintainer: fre@freux.fr
+author: Valentin Reis
+synopsis: Dhall/YAML configurable concurrent integration test executor.
+description:
+    `dhrun` starts a list of (Unix) processes, monitors the standard streams for patterns that should be expected or avoided, kills the processes when criteria are met and exits accordingly. It is configured using either [Dhall](https://dhall-lang.org/) or [YAML](https://yaml.org/). See the [README.md](https://github.com/freuk/dhrun) file for details.
+category: tools
+build-type: Simple
+extra-source-files:
+    ChangeLog.md
+
+source-repository head
+    type: git
+    location: https://github.com/freuk/dhrun
+
+library dhrun-lib
+    exposed-modules:
+        Dhrun.Types.Cfg
+        Dhrun.Types.Dhall
+        Dhrun.Types.Yaml
+        Dhrun.Run
+        Dhrun.Pure
+        Dhrun.Conduit
+    hs-source-dirs: src
+    default-language: Haskell2010
+    default-extensions: LambdaCase RecordWildCards ScopedTypeVariables
+                        NoImplicitPrelude OverloadedStrings ViewPatterns
+    ghc-options: -Wall -Wcompat -Wincomplete-uni-patterns
+                 -Wincomplete-record-updates -Wmissing-home-modules -Widentities
+                 -Wredundant-constraints -Wcpp-undef -fwarn-tabs
+                 -fwarn-unused-imports -fwarn-missing-signatures
+                 -fwarn-name-shadowing -fprint-potential-instances
+                 -Wmissing-export-lists -fwarn-unused-do-bind -fwarn-wrong-do-bind
+                 -fwarn-incomplete-patterns
+    build-depends:
+        base (==4.11.1 || >4.11.1) && <4.12,
+        bytestring (==0.10.8 || >0.10.8) && <0.11,
+        containers (==0.5.11 || >0.5.11) && <0.6,
+        text (==1.2.3 || >1.2.3) && <1.3,
+        unix (==2.7.2 || >2.7.2) && <2.8,
+        time (==1.8.0 || >1.8.0) && <1.9,
+        ansi-terminal (==0.9.1 || >0.9.1) && <0.10,
+        conduit (==1.3.0 || >1.3.0) && <1.4,
+        directory (==1.3.1 || >1.3.1) && <1.4,
+        mtl (==2.2.2 || >2.2.2) && <2.3,
+        unliftio-core (==0.1.2 || >0.1.2) && <0.2,
+        conduit-extra (==1.3.0 || >1.3.0) && <1.4,
+        process (==1.6.3 || >1.6.3) && <1.7,
+        dhall (==1.24.0 || >1.24.0) && <1.25,
+        protolude (==0.2.2 || >0.2.2) && <0.3,
+        yaml (==0.8.32 || >0.8.32) && <0.9
+
+executable dhrun
+    main-is: Main.hs
+    hs-source-dirs: app
+    default-language: Haskell2010
+    default-extensions: LambdaCase RecordWildCards ScopedTypeVariables
+                        NoImplicitPrelude OverloadedStrings ViewPatterns
+    ghc-options: -Wall -Wcompat -Wincomplete-uni-patterns
+                 -Wincomplete-record-updates -Wmissing-home-modules -Widentities
+                 -Wredundant-constraints -Wcpp-undef -fwarn-tabs
+                 -fwarn-unused-imports -fwarn-missing-signatures
+                 -fwarn-name-shadowing -fprint-potential-instances
+                 -Wmissing-export-lists -fwarn-unused-do-bind -fwarn-wrong-do-bind
+                 -fwarn-incomplete-patterns -threaded
+    build-depends:
+        base (==4.11.1 || >4.11.1) && <4.12,
+        bytestring (==0.10.8 || >0.10.8) && <0.11,
+        directory (==1.3.1 || >1.3.1) && <1.4,
+        filepath (==1.4.2 || >1.4.2) && <1.5,
+        dhall (==1.24.0 || >1.24.0) && <1.25,
+        optparse-applicative (==0.15.0 || >0.15.0) && <0.16,
+        protolude (==0.2.2 || >0.2.2) && <0.3,
+        editor-open (==0.6.0 || >0.6.0) && <0.7
+
+test-suite Tests
+    type: exitcode-stdio-1.0
+    main-is: Tests.hs
+    hs-source-dirs: tests
+    default-language: Haskell2010
+    default-extensions: LambdaCase RecordWildCards ScopedTypeVariables
+                        NoImplicitPrelude OverloadedStrings ViewPatterns
+    ghc-options: -Wall -Wcompat -Wincomplete-uni-patterns
+                 -Wincomplete-record-updates -Wmissing-home-modules -Widentities
+                 -Wredundant-constraints -Wcpp-undef -fwarn-tabs
+                 -fwarn-unused-imports -fwarn-missing-signatures
+                 -fwarn-name-shadowing -fprint-potential-instances
+                 -Wmissing-export-lists -fwarn-unused-do-bind -fwarn-wrong-do-bind
+                 -fwarn-incomplete-patterns -threaded
+    build-depends:
+        base (==4.11.1 || >4.11.1) && <4.12,
+        protolude (==0.2.2 || >0.2.2) && <0.3,
+        dhall (==1.24.0 || >1.24.0) && <1.25,
+        yaml (==0.8.32 || >0.8.32) && <0.9,
+        aeson -any,
+        filepath (==1.4.2 || >1.4.2) && <1.5,
+        mtl (==2.2.2 || >2.2.2) && <2.3,
+        bytestring (==0.10.8 || >0.10.8) && <0.11,
+        text (==1.2.3 || >1.2.3) && <1.3,
+        unliftio -any,
+        tasty -any,
+        tasty-hunit -any,
+        tasty-golden -any,
+        tasty-hspec -any,
+        tasty-quickcheck -any,
+        generic-random -any,
+        quickcheck-text -any,
+        hspec -any,
+        dhrun-lib -any,
+        Glob -any
diff --git a/src/Dhrun/Conduit.hs b/src/Dhrun/Conduit.hs
new file mode 100644
--- /dev/null
+++ b/src/Dhrun/Conduit.hs
@@ -0,0 +1,128 @@
+{-# language DataKinds #-}
+{-# language FlexibleContexts #-}
+{-|
+Module      : Dhrun.Run
+Description : runner
+Copyright   : (c) Valentin Reis, 2018
+License     : MIT
+Maintainer  : fre@freux.fr
+-}
+
+module Dhrun.Conduit
+  ( monitor
+  , Sink
+  , Source
+  , P
+  , PC
+  )
+where
+
+import           Dhrun.Types.Cfg
+import           Dhrun.Pure
+import           Protolude
+import qualified Data.ByteString                                   as B
+import           Control.Exception.Base
+                   ( throw )
+import qualified Data.Conduit.Binary                               as CB
+                   ( lines )
+import qualified Data.Conduit.Combinators                          as CC
+                   ( unlinesAscii )
+import           Data.Conduit
+                   ( ConduitT
+                   , yield
+                   , await
+                   , runConduit
+                   , (.|)
+                   )
+import qualified Data.Conduit.Process.Typed                        as PT
+                   ( Process
+                   , ProcessConfig
+                   )
+import qualified Data.Conduit.Lift                                 as CLF
+import           Data.Time.Clock.POSIX
+
+type Source = ConduitT () ByteString IO ()
+type Sink = ConduitT ByteString Void IO ()
+type P  = PT.Process () Source Source
+type PC = PT.ProcessConfig () Source Source
+
+-- | output monitoring conduit. THROWS PatternMatched - with a one second wait to
+monitor
+  :: Check
+  -> ConduitT () ByteString IO ()
+  -> ConduitT ByteString Void IO ()
+  -> IO ()
+monitor behavior source sink =
+  runConduit
+    $  source
+    .| CB.lines
+    .| void (CLF.runStateC initialState (CLF.runReaderC behavior makeBehavior))
+    .| CC.unlinesAscii
+    .| sink
+
+initialState :: MonitoringState
+initialState = Nothing
+type MonitoringState = Maybe Matched
+data Matched = Matched
+  { matched :: Text
+  , atTime :: POSIXTime } deriving (Show)
+
+-- | makeBehavior builds an IO conduit that throws a PatternMatched when
+-- all wanted pattern or one avoided pattern are found
+makeBehavior
+  :: (MonadIO m, MonadState MonitoringState m, MonadReader Check m)
+  => ConduitT ByteString ByteString m ()
+makeBehavior = wants <$> ask >>= \case
+  [] -> cleanLooper
+  as -> expectfulLooper $ toS <$> as
+
+expectfulLooper
+  :: (MonadIO m, MonadState MonitoringState m, MonadReader Check m)
+  => [ByteString]
+  -> ConduitT ByteString ByteString m ()
+expectfulLooper [] = throw ThrowFoundAllWants
+expectfulLooper l  = noAvoidsAndOtherwise $ \b ->
+  yield b >> expectfulLooper (filter (not . flip B.isInfixOf b) (toS <$> l))
+
+cleanLooper
+  :: (MonadIO m, MonadState MonitoringState m, MonadReader Check m)
+  => ConduitT ByteString ByteString m ()
+cleanLooper = noAvoidsAndOtherwise $ \b -> yield b >> cleanLooper
+
+noAvoidsAndOtherwise
+  :: (MonadIO m, MonadState MonitoringState m, MonadReader Check m)
+  => (ByteString -> ConduitT ByteString ByteString m ())
+  -> ConduitT ByteString ByteString m ()
+noAvoidsAndOtherwise otherwiseConduit = do
+  av <- avoids <$> ask
+  t  <- liftIO getPOSIXTime
+  get >>= \case
+    Just m  -> goThrow t m
+    Nothing -> return ()
+  await >>= \case
+    Just b -> case filter (`B.isInfixOf` b) (toS <$> av) of
+      []     -> otherwiseConduit b
+      xh : _ -> do
+        put . Just $ Matched (toS xh) t
+        otherwiseConduit b
+    Nothing -> return ()
+ where
+  goThrow :: POSIXTime -> Matched -> ConduitT ByteString ByteString m ()
+  goThrow t Matched {..} =
+    when (t > 1 + atTime) (throw $ ThrowFoundAnAvoid matched)
+
+{-noAvoidsAndOtherwise-}
+  {-:: (MonadIO m)-}
+  {-=> Maybe (Integer, a)-}
+  {--> Check-}
+  {--> (Maybe (Integer, a) -> ByteString -> ConduitT ByteString ByteString m ())-}
+  {--> ConduitT ByteString ByteString m ()-}
+{-noAvoidsAndOtherwise mt Check {..} otherwiseConduit = do-}
+  {-t <- liftIO askPOSIXTime <&> toInteger-}
+  {-case shouldThrow t mt of-}
+    {-Just xh -> throw $ ThrowFoundAnAvoid $ toS xh-}
+    {-Nothing -> await >>= \case-}
+      {-Just b -> case filter (`B.isInfixOf` b) (toS <$> avoids) of-}
+        {-[]     -> otherwiseConduit t b-}
+        {-xh : _ -> otherwiseConduit t b-}
+      {-Nothing -> return ()-}
diff --git a/src/Dhrun/Pure.hs b/src/Dhrun/Pure.hs
new file mode 100644
--- /dev/null
+++ b/src/Dhrun/Pure.hs
@@ -0,0 +1,171 @@
+{-# language RecordWildCards #-}
+{-# language DeriveGeneric #-}
+
+{-|
+Module      : Dhrun.Run
+Description : runner
+Copyright   : (c) Valentin Reis, 2018
+License     : MIT
+Maintainer  : fre@freux.fr
+-}
+
+module Dhrun.Pure
+  ( ProcessWas(..)
+  , CmdResult(..)
+  , MonitoringResult(..)
+  , Std(..)
+  , envVars
+  , concludeCmd
+  , finalizeCmd
+  , getWdFilename
+  , stdToS
+  , noWants
+  , with3
+  , with2
+  , mapTuple
+  )
+where
+
+import           Dhrun.Types.Cfg
+import           Protolude
+import qualified Data.Map.Lazy                                     as DM
+                   ( fromList
+                   , toList
+                   )
+import qualified Data.Map.Merge.Lazy                               as DMM
+                   ( merge
+                   , preserveMissing
+                   , zipWithMatched
+                   )
+import qualified Data.Text                                         as T
+                   ( lines )
+import           Control.Arrow
+                   ( (***) )
+
+data CmdResult =
+    Timeout Cmd -- | if the command died with a timeout
+  | DiedLegal Cmd -- | if the command wasnt expected to do anything and died 1
+  | ThrewException Cmd Text
+  | DiedFailure Cmd Int
+  | DiedExpected Cmd -- | if the command was expected in exitcode x and died x
+  | DiedUnExpected Cmd ExitCode -- | command expected in exitcode x and died y!=x
+  | FoundAll Cmd
+  | FoundIllegal Cmd Text Std
+  | OutputLacking Cmd Std
+  | ConduitException Cmd Std
+  deriving (Show,Generic)
+
+data MonitoringResult =
+    ThrowFoundAllWants
+  | ThrowFoundAnAvoid Text
+  deriving (Show, Typeable)
+instance Exception MonitoringResult
+
+data ProcessWas = Died ExitCode | Killed
+
+data Std = Out | Err deriving (Show, Generic)
+
+-- | concludeCmd hadNoWants cmdresult returns dhrun's conclusion based on whether
+-- there were any "wants" in the template.
+concludeCmd :: Bool -> CmdResult -> Either [Text] Text
+concludeCmd True  (DiedLegal _) = Right "All commands exited successfully."
+concludeCmd False (DiedLegal c) = Left
+  [ "command exited:"
+      <> mconcat (intersperse "\n" (T.lines (toS $ encodeCmd c)))
+  ]
+concludeCmd _ (Timeout c) =
+  Left $ "The following command timed out:" : T.lines (toS $ encodeCmd c)
+concludeCmd _ (DiedFailure c n) =
+  Left
+    $  "The following command died with exit code "
+    <> show n
+    <> " :"
+    :  T.lines (toS $ encodeCmd c)
+concludeCmd _ (FoundAll c) = Right
+  ("All searched patterns in the following command were found. Killing all processes.\n "
+  <> mconcat (intersperse "\n" (T.lines (toS $ encodeCmd c)))
+  )
+concludeCmd _ (FoundIllegal c t e) =
+  Left
+    $  "The illegal pattern "
+    <> t
+    <> " was found in the output of this process' "
+    <> stdToS e
+    <> ":"
+    :  T.lines (toS $ encodeCmd c)
+concludeCmd _ (OutputLacking c e) =
+  Left
+    $  "This process' "
+    <> stdToS e
+    <> " was found to be lacking pattern(s):"
+    :  T.lines (toS $ encodeCmd c)
+concludeCmd _ (ConduitException c e) =
+  Left $ "This process ended with a conduit exception:" <> stdToS e : T.lines
+    (toS $ encodeCmd c)
+concludeCmd _ (ThrewException c e) =
+  Left $ "This process' execution ended with an exception: " <> e : T.lines
+    (toS $ encodeCmd c)
+concludeCmd _ (DiedUnExpected c n) =
+  Left $ "process exited with inadequate exit code " <> show n <> ": " : T.lines
+    (toS $ encodeCmd c)
+concludeCmd _ (DiedExpected c) =
+  Right $ "process exited with adequate exit code " <> ": " <> toS (encodeCmd c)
+
+stdToS :: Std -> Text
+stdToS Out = "stdout"
+stdToS Err = "stderr"
+
+noWants :: Cmd -> Bool
+noWants Cmd {..} = null (wants $ filecheck out) && null (wants $ filecheck err)
+
+envVars :: [EnvVar] -> [VarName] -> [(Text, Text)] -> [(Text, Text)]
+envVars internEnv passVars externEnv = DM.toList $ DMM.merge
+  DMM.preserveMissing
+  DMM.preserveMissing
+  (DMM.zipWithMatched (\_ x _ -> x))
+  (DM.fromList (forcedEnvVars internEnv))
+  (DM.fromList (externEnvVars passVars externEnv))
+
+externEnvVars :: [VarName] -> [(Text, Text)] -> [(Text, Text)]
+externEnvVars passvars = filter (\(k, _) -> k `elem` (toS <$> passvars))
+
+forcedEnvVars :: [EnvVar] -> [(Text, Text)]
+forcedEnvVars vs = (\EnvVar {..} -> (toS varname, toS value)) <$> vs
+
+mapTuple :: (a -> b) -> (a, a) -> (b, b)
+mapTuple = join (***)
+
+getWdFilename :: Text -> FileCheck a -> FilePath
+getWdFilename wdT fc = toS $ wdT <> "/" <> toS (filename fc)
+
+finalizeCmd
+  :: Cmd
+  -> Either (Either SomeException ()) (Either SomeException ())
+  -> ProcessWas
+  -> CmdResult
+finalizeCmd c _ (Died ec) = case exitcode c of
+  Nothing -> DiedLegal c
+  Just ecExpect ->
+    if ecExpect == ec then DiedExpected c else DiedUnExpected c ec
+finalizeCmd c ei _ = rightFinalizer ei
+ where
+  rightFinalizer (Left  (Right ())) = legalOrLacking Out
+  rightFinalizer (Right (Right ())) = legalOrLacking Err
+  rightFinalizer (Left  (Left  e )) = unpackE e Out
+  rightFinalizer (Right (Left  e )) = unpackE e Err
+  unpackE e r = case fromException e of
+    Just (ThrowFoundAnAvoid t) -> FoundIllegal c t r
+    Just ThrowFoundAllWants    -> FoundAll c
+    Nothing                    -> ConduitException c r
+  legalOrLacking r = if noWants c then DiedLegal c else OutputLacking c r
+
+with3
+  :: ((t1 -> t2) -> t3)
+  -> ((t4 -> t5) -> t2)
+  -> ((t6 -> t7) -> t5)
+  -> (t1 -> t4 -> t6 -> t7)
+  -> t3
+with3 w1 w2 w3 f = w1 $ \v1 -> w2 $ \v2 -> w3 $ \v3 -> f v1 v2 v3
+
+with2 :: ((t1 -> t2) -> t3) -> ((t4 -> t5) -> t2) -> (t1 -> t4 -> t5) -> t3
+with2 w1 w2 f = w1 $ \v1 -> w2 $ \v2 -> f v1 v2
diff --git a/src/Dhrun/Run.hs b/src/Dhrun/Run.hs
new file mode 100644
--- /dev/null
+++ b/src/Dhrun/Run.hs
@@ -0,0 +1,244 @@
+{-# language FlexibleContexts #-}
+{-# language TypeOperators #-}
+
+{-|
+Module      : Dhrun.Run
+Description : runner
+Copyright   : (c) Valentin Reis, 2018
+License     : MIT
+Maintainer  : fre@freux.fr
+-}
+
+module Dhrun.Run
+  ( runDhrun
+  )
+where
+
+import           Dhrun.Types.Cfg
+import           Dhrun.Pure
+import           Dhrun.Conduit
+import           Protolude
+import           System.Exit
+                   ( ExitCode(..) )
+import           Data.Conduit
+                   ( ConduitT )
+import qualified System.Posix.Signals                              as SPS
+                   ( installHandler
+                   , keyboardSignal
+                   , Handler(..)
+                   )
+import qualified Data.Conduit.Process.Typed                        as PT
+import qualified Data.Conduit.Binary                               as CB
+import qualified Data.Text                                         as T
+                   ( isInfixOf )
+import qualified System.IO                                         as SIO
+                   ( BufferMode(NoBuffering)
+                   , hSetBuffering
+                   , withBinaryFile
+                   , IOMode(WriteMode)
+                   , stdout
+                   )
+
+import           Control.Monad.IO.Unlift
+                   ( MonadIO(..)
+                   , withRunInIO
+                   )
+import           Control.Monad.Writer
+                   ( MonadWriter
+                   , tell
+                   , runWriterT
+                   )
+import qualified System.Directory                                  as SD
+                   ( createDirectoryIfMissing )
+import qualified System.Environment                                as SE
+                   ( getEnvironment )
+import qualified System.Timeout                                    as ST
+
+import           System.Console.ANSI
+
+putC :: MonadIO m => Text -> Color -> m ()
+putC content color = setC color *> putT content *> setC White
+ where
+  setC c = liftIO $ setSGR [SetColor Foreground Dull c]
+  putT :: MonadIO m => Text -> m ()
+  putT = putStr
+
+-- | runDhrun d runs a dhrun specification in the lifted IO monad.
+runDhrun :: (MonadIO m) => Cfg -> m ()
+runDhrun dhallExec =
+  runWriterT (runReaderT runAllSteps dhallExec) <&> snd >>= liftIO . \case
+    [] -> putC "Success. " Green
+      <> putText "No errors were encountered and all requirements were met."
+    errors ->
+      putText "Error log:"
+        <> for_ errors Protolude.putText
+        <> putC "Failure. " Red
+        <> die "exiting."
+
+runAllSteps :: (MonadIO m, MonadReader Cfg m, MonadWriter [Text] m) => m ()
+runAllSteps = do
+  liftIO $ SIO.hSetBuffering SIO.stdout SIO.NoBuffering
+  runWorkDir -- | setting up the working directory
+  runPre     -- | preprocessing steps
+  runAsyncs  -- | running the main async step
+  runChecks  -- | running file checks
+  runPost    -- | postprocessing steps
+ where
+  runPre  = runMultipleV ((toS <$>) <$> pre) "pre-processing"
+  runPost = runMultipleV ((toS <$>) <$> post) "post-processing"
+
+putV :: (MonadIO m, MonadReader Cfg m) => Text -> m ()
+putV text =
+  (== Verbose) . verbosity <$> ask >>= flip when (liftIO $ putText text)
+
+runWorkDir :: (MonadIO m, MonadReader Cfg m) => m ()
+runWorkDir = do
+  (shouldRemove, wd) <- ask <&> \a -> (Remove == cleaning a, toS $ workdir a)
+  when shouldRemove
+    $  PT.runProcess_
+    $  PT.shell
+    $  toS
+    $  ("rm -rf " :: Text)
+    <> toS wd
+  liftIO $ SD.createDirectoryIfMissing False wd
+
+runChecks :: (MonadIO m, MonadReader Cfg m, MonadWriter [Text] m) => m ()
+runChecks = (cmds <$> ask) >>= \case
+  [] -> pu "no processes"
+  l  -> do
+    pu "start"
+    for_ l $ \Cmd {..} -> do
+      pu $ "running post-mortem checks for " <> toS name <> " " <> mconcat
+        (intersperse " " (toS <$> args))
+      for_ postchecks $ \FileCheck {..} -> do
+        contents <- liftIO $ readFile (toS filename)
+        forM_ (wants filecheck) $ \(Pattern x) -> unless
+          (T.isInfixOf x contents)
+          (tell
+            [ "post-mortem check fail: "
+              <> x
+              <> " not found in file "
+              <> toS filename
+            ]
+          )
+        forM_ (avoids filecheck) $ \(Pattern x) -> when
+          (T.isInfixOf x contents)
+          (tell
+            [ "post-mortem check fail: "
+              <> x
+              <> " found in file "
+              <> toS filename
+            ]
+          )
+    pu " done"
+  where pu t = putV $ "check step:" <> t
+
+runAsyncs :: (MonadIO m, MonadReader Cfg m, MonadWriter [Text] m) => m ()
+runAsyncs = (cmds <$> ask) >>= \case
+  [] -> pu "async step: no processes."
+  l  -> do
+    pu "async step: start"
+    wd        <- workdir <$> ask
+    externEnv <- liftIO SE.getEnvironment <&> (mapTuple toS <$>)
+    asyncs    <- liftIO $ mkAsyncs l externEnv wd
+    _         <- kbInstallHandler $ for_ asyncs cancel
+    pu "async step: started processes"
+    liftIO (waitAnyCancel asyncs)
+      <&> snd
+      <&> concludeCmd (any noWants l)
+      >>= either tell putText
+    pu "async step: done"
+ where
+  pu t = putV $ "async step:" <> t
+  mkAsyncs :: [Cmd] -> [(Text, Text)] -> WorkDir -> IO [Async CmdResult]
+  mkAsyncs l externEnv wd = for l (async . runCmd externEnv wd)
+  kbInstallHandler :: (MonadIO m) => IO () -> m SPS.Handler
+  kbInstallHandler h =
+    liftIO $ SPS.installHandler SPS.keyboardSignal (SPS.Catch h) Nothing
+
+runMultipleV
+  :: (MonadIO m, MonadReader Cfg m) => (Cfg -> [Text]) -> Text -> m ()
+runMultipleV getter desc = getter <$> ask >>= \case
+  [] -> pu $ "no " <> desc <> " commands to run."
+  l  -> do
+    pu $ desc <> ": start"
+    wdirFP <- ask <&> workdir <&> \(WorkDir wdt) -> wdt
+    for_ l $ runOne wdirFP
+    pu $ desc <> ": done"
+ where
+  pu t = putV $ desc <> " step:" <> t
+  runOne wdirFP x =
+    liftIO (PT.runProcess $ PT.setWorkingDir (toS wdirFP) (PT.shell $ toS x))
+      >>= \case
+            ExitSuccess -> pu ("ran one " <> desc <> " command.")
+            ExitFailure _ ->
+              liftIO
+                $  die
+                $  "failed to execute one "
+                <> desc
+                <> " command."
+                <> x
+
+-- | Runs a single command. should be the only 'complex' function in this codebaseaside from runAsyncs. TODO: add the exitcode check functionality.
+runCmd :: [(Text, Text)] -> WorkDir -> Cmd -> IO CmdResult
+runCmd fullExternEnv (WorkDir wd) c@Cmd {..} = do
+  for_ otherwd $ \(WorkDir swd) -> SD.createDirectoryIfMissing True (toS swd)
+  fromMaybe (Timeout c) <$> maybeTimeout go timeout
+ where
+  realwd = maybe (toS wd) toS otherwd
+
+  -- | output files
+  outFile :: FilePath
+  outFile = getWdFilename realwd out
+  errFile :: FilePath
+  errFile = getWdFilename realwd err
+
+  -- | process configuration
+  pc :: PC
+  pc =
+    PT.proc (toS name) (toS <$> args)
+      & PT.setWorkingDir (toS realwd)
+      & PT.setEnv (mapTuple toS <$> envVars vars passvars fullExternEnv)
+      & PT.setStdout PT.createSource
+      & PT.setStderr PT.createSource
+      & PT.setStdin PT.closed
+
+  -- | lambda-scoped resource acquisition: stdin/out conduits and starting the process
+  go :: IO CmdResult
+  go = with3 (withSinkFileNoBuffering outFile)
+             (withSinkFileNoBuffering errFile)
+             (withWrappedSafeP c pc)
+             logic
+
+  -- | the main logic, that uses two sinks and the started process to
+  -- do the streaming monitoring.
+  logic :: Sink -> Sink -> P -> IO CmdResult
+  logic sout serr p =
+    finalizeCmd c
+      <$> with2 (withAsync $ monitor (filecheck out) (PT.getStdout p) sout)
+                (withAsync $ monitor (filecheck err) (PT.getStderr p) serr)
+                waitEitherCatchCancel
+      <*> (PT.getExitCode p >>= \case
+            Nothing -> Killed <$ PT.stopProcess p
+            Just ec -> return $ Died ec
+          )
+
+-- | May timeout an IO command.
+maybeTimeout :: IO a -> Maybe Int -> IO (Maybe a)
+maybeTimeout io = maybe (Just <$> io) (\t -> ST.timeout (1000000 * t) io)
+
+-- | runs an IO action with a preconfigured process (PC) in lambda scope (P)
+withWrappedSafeP :: Cmd -> PC -> (P -> IO CmdResult) -> IO CmdResult
+withWrappedSafeP c pc f = catchIOE $ PT.withProcess pc f
+ where
+  catchIOE :: IO CmdResult -> IO CmdResult
+  catchIOE ioOp =
+    catch ioOp $ \(e :: IOException) -> return $ ThrewException c (show e)
+
+-- | runs an IO action with a file sink in lambda scope
+withSinkFileNoBuffering
+  :: FilePath -> (ConduitT ByteString o IO () -> IO b) -> IO b
+withSinkFileNoBuffering filepath lambdaIO =
+  withRunInIO $ \run -> SIO.withBinaryFile filepath SIO.WriteMode $ \h -> do
+    SIO.hSetBuffering h SIO.NoBuffering
+    run $ lambdaIO $ CB.sinkHandle h
diff --git a/src/Dhrun/Types/Cfg.hs b/src/Dhrun/Types/Cfg.hs
new file mode 100644
--- /dev/null
+++ b/src/Dhrun/Types/Cfg.hs
@@ -0,0 +1,222 @@
+{-# language MultiParamTypeClasses #-}
+{-# language FlexibleInstances #-}
+{-# language DeriveGeneric #-}
+
+{-|
+Module      : Dhrun.Internal
+Description : types
+Copyright   : (c) Valentin Reis, 2018
+License     : MIT
+Maintainer  : fre@freux.fr
+-}
+
+module Dhrun.Types.Cfg
+  ( Cfg(..)
+  , Verbosity(..)
+  , Cmd(..)
+  , EnvVar(..)
+  , VarName(..)
+  , VarValue(..)
+  , WorkDir(..)
+  , WorkDirBehavior(..)
+  , Check(..)
+  , FileCheck(..)
+  , FileName(..)
+  , CommandName(..)
+  , Arg(..)
+  , Pattern(..)
+  , inputCfg
+  , decodeCfgFile
+  , decodeCfg
+  , encodeCfg
+  , encodeCmd
+  )
+where
+
+import           Dhall
+import qualified Dhrun.Types.Dhall             as DT
+import qualified Dhrun.Types.Yaml              as DAT
+import           Data.Yaml.Internal
+import           Protolude
+import qualified Prelude                        ( String )
+
+newtype Arg = Arg Text deriving (Eq, Show, Generic)
+instance StringConv Arg Text where
+  strConv _ (Arg x) = toS x
+instance StringConv Arg Prelude.String where
+  strConv _ (Arg x) = toS x
+
+newtype CommandName = CommandName Text deriving (Eq, Show, Generic)
+instance StringConv CommandName Text where
+  strConv _ (CommandName x) = toS x
+instance StringConv CommandName FilePath where
+  strConv _ (CommandName x) = toS x
+
+newtype Pattern = Pattern Text deriving (Eq, Show, Generic)
+instance StringConv Pattern ByteString where
+  strConv _ (Pattern x) = toS x
+instance StringConv Pattern Text where
+  strConv _ (Pattern x) = toS x
+
+newtype FileName = FileName Text deriving (Eq, Show, Generic)
+instance StringConv FileName Text where
+  strConv _ (FileName x) = toS x
+instance StringConv FileName FilePath where
+  strConv _ (FileName x) = toS x
+
+newtype VarName = VarName Text deriving (Eq, Show, Generic)
+instance StringConv VarName Text where
+  strConv _ (VarName x) = toS x
+
+newtype VarValue = VarValue Text deriving (Eq, Show, Generic)
+instance StringConv VarValue Text where
+  strConv _ (VarValue x) = toS x
+
+newtype Pre = Pre Text deriving (Eq, Show, Generic)
+instance StringConv Pre Text where
+  strConv _ (Pre x) = toS x
+
+newtype Post = Post Text deriving (Eq, Show, Generic)
+instance StringConv Post Text where
+  strConv _ (Post x) = toS x
+
+newtype WorkDir = WorkDir Text deriving (Eq, Show, Generic)
+instance StringConv WorkDir Text where
+  strConv _ (WorkDir x) = toS x
+instance StringConv WorkDir FilePath where
+  strConv _ (WorkDir x) = toS x
+
+data WorkDirBehavior = Keep | Remove
+  deriving (Read, Show, Eq)
+
+data Verbosity = Normal | Verbose
+  deriving (Read, Show, Eq)
+
+data EnvVar = EnvVar {
+    varname :: VarName
+  , value   :: VarValue
+  } deriving (Eq, Show, Generic)
+
+
+data Check = Check {
+    avoids :: [Pattern]
+  , wants  :: [Pattern]
+  } deriving (Eq, Show, Generic)
+
+data FileCheck a = FileCheck {
+    filename  :: FileName
+  , filecheck :: a
+  } deriving (Eq, Show, Generic)
+
+data Cmd = Cmd {
+    name        :: CommandName
+  , exitcode    :: Maybe ExitCode
+  , args        :: [Arg]
+  , vars        :: [EnvVar]
+  , passvars    :: [VarName]
+  , out         :: FileCheck Check
+  , err         :: FileCheck Check
+  , postchecks  :: [FileCheck Check]
+  , timeout     :: Maybe Int
+  , otherwd     :: Maybe WorkDir
+  } deriving (Eq, Show, Generic)
+
+data Cfg = Cfg
+  { cmds      :: [Cmd],
+    workdir   :: WorkDir,
+    cleaning  :: WorkDirBehavior,
+    verbosity :: Verbosity,
+    pre       :: [Pre],
+    post      :: [Post]
+  } deriving (Show, Eq)
+
+toInternal :: DT.Cfg -> Cfg
+toInternal DT.Cfg {..} = Cfg
+  { cmds      = toInternalCmd <$> cmds
+  , workdir   = WorkDir workdir
+  , cleaning  = if cleaning then Remove else Keep
+  , pre       = Pre <$> pre
+  , post      = Post <$> post
+  , verbosity = if verbose then Verbose else Normal
+  }
+
+fromInternal :: Cfg -> DT.Cfg
+fromInternal Cfg {..} = DT.Cfg
+  { cmds     = fromInternalCmd <$> cmds
+  , workdir  = toS workdir
+  , cleaning = cleaning == Remove
+  , pre      = toS <$> pre
+  , post     = toS <$> post
+  , verbose  = verbosity == Verbose
+  }
+
+toInternalCmd :: DT.Cmd -> Cmd
+toInternalCmd DT.Cmd {..} = Cmd
+  { name       = CommandName name
+  , exitcode   = case exitcode of
+                   Just 0 -> Just ExitSuccess
+                   Just x -> Just $ ExitFailure (fromInteger x)
+                   Nothing -> Nothing
+  , args       = Arg <$> args
+  , vars       = vars <&> \DT.EnvVar {..} ->
+    EnvVar {varname = VarName varname, value = VarValue value}
+  , passvars   = VarName <$> passvars
+  , out        = toInternalFileCheck out
+  , err        = toInternalFileCheck err
+  , postchecks = toInternalFileCheck <$> postchecks
+  , timeout    = fromInteger . toInteger <$> timeout
+  , otherwd      = WorkDir <$> otherwd
+  }
+
+toInternalFileCheck :: DT.FileCheck DT.Check -> FileCheck Check
+toInternalFileCheck DT.FileCheck {..} = FileCheck
+  { filename  = FileName filename
+  , filecheck = toInternalCheck filecheck
+  }
+
+toInternalCheck :: DT.Check -> Check
+toInternalCheck DT.Check {..} =
+  Check {avoids = Pattern <$> avoids, wants = Pattern <$> wants}
+
+fromInternalCmd :: Cmd -> DT.Cmd
+fromInternalCmd Cmd {..} = DT.Cmd
+  { out        = fromInternalFileCheck out
+  , err        = fromInternalFileCheck err
+  , postchecks = fromInternalFileCheck <$> postchecks
+  , timeout    = fromInteger . toInteger <$> timeout
+  , name       = toS name
+  , exitcode   = case exitcode of
+                   Just ExitSuccess -> Just 1
+                   Just (ExitFailure n) -> Just $ toInteger n
+                   Nothing -> Nothing
+  , args       = toS <$> args
+  , vars       = vars <&> \case
+    EnvVar {..} -> DT.EnvVar {varname = toS varname, value = toS value}
+  , passvars   = toS <$> passvars
+  , otherwd      = toS <$> otherwd
+  }
+
+fromInternalFileCheck :: FileCheck Check -> DT.FileCheck DT.Check
+fromInternalFileCheck FileCheck {..} = DT.FileCheck
+  { filename  = toS filename
+  , filecheck = fromInternalCheck filecheck
+  }
+
+fromInternalCheck :: Check -> DT.Check
+fromInternalCheck Check {..} =
+  DT.Check {avoids = toS <$> avoids, wants = toS <$> wants}
+
+decodeCfgFile :: (MonadIO m) => Text -> m Cfg
+decodeCfgFile fn = toInternal <$> DAT.decodeCfgFile fn
+
+decodeCfg :: ByteString -> Either Data.Yaml.Internal.ParseException Cfg
+decodeCfg t = toInternal <$> DAT.decodeCfg t
+
+encodeCfg :: Cfg -> ByteString
+encodeCfg = DAT.encodeCfg . fromInternal
+
+encodeCmd :: Cmd -> ByteString
+encodeCmd = DAT.encodeCmd . fromInternalCmd
+
+inputCfg :: (MonadIO m) => Text -> m Cfg
+inputCfg fn = toInternal <$> DT.inputCfg fn
diff --git a/src/Dhrun/Types/Dhall.hs b/src/Dhrun/Types/Dhall.hs
new file mode 100644
--- /dev/null
+++ b/src/Dhrun/Types/Dhall.hs
@@ -0,0 +1,73 @@
+{-# language DeriveAnyClass #-}
+{-# language DeriveFunctor #-}
+{-# language DeriveGeneric #-}
+
+{-|
+Module      : Dhrun.Types
+Description : types
+Copyright   : (c) Valentin Reis, 2018
+License     : MIT
+Maintainer  : fre@freux.fr
+-}
+
+module Dhrun.Types.Dhall
+  ( Cfg(..)
+  , Cmd(..)
+  , EnvVar(..)
+  , Check(..)
+  , FileCheck(..)
+  , inputCfg
+  )
+where
+
+import           Protolude
+import           Dhall
+import           Data.Yaml
+import           Data.Aeson
+
+data EnvVar = EnvVar {
+    varname :: Text
+  , value   :: Text
+  } deriving (Eq, Show, Generic, ToJSON, Interpret)
+instance FromJSON EnvVar where
+    parseJSON = genericParseJSON defaultOptions { omitNothingFields  = True }
+
+data Check = Check {
+    avoids :: [Text]
+  , wants  :: [Text]
+  } deriving (Eq, Show, Generic, ToJSON, Interpret)
+
+data Cfg = Cfg
+  { cmds      :: [Cmd],
+    workdir   :: Text,
+    cleaning  :: Bool,
+    verbose   :: Bool,
+    pre       :: [Text],
+    post      :: [Text]
+  } deriving (Show, Eq, Generic, Interpret)
+
+data FileCheck a = FileCheck {
+    filename  :: Text
+  , filecheck :: a
+  } deriving (Eq, Show, Generic, ToJSON, Interpret, Functor)
+
+data Cmd = Cmd {
+    name        :: Text
+  , exitcode    :: Maybe Integer
+  , args        :: [Text]
+  , vars        :: [EnvVar]
+  , otherwd     :: Maybe Text
+  , passvars    :: [Text]
+  , out         :: FileCheck Check
+  , err         :: FileCheck Check
+  , postchecks  :: [FileCheck Check]
+  , timeout     :: Maybe Natural
+  } deriving (Eq, Show, Generic, ToJSON, Interpret)
+
+inputCfg :: (MonadIO m) => Text -> m Cfg
+inputCfg fn = liftIO $ try (input ft fn) >>= \case
+  Right d -> return d
+  Left  e -> throwError e
+ where
+  ft :: Dhall.Type Cfg
+  ft = Dhall.auto
diff --git a/src/Dhrun/Types/Yaml.hs b/src/Dhrun/Types/Yaml.hs
new file mode 100644
--- /dev/null
+++ b/src/Dhrun/Types/Yaml.hs
@@ -0,0 +1,153 @@
+{-# language DeriveAnyClass #-}
+{-# language FlexibleInstances #-}
+{-# language FlexibleContexts #-}
+{-# language DeriveGeneric #-}
+
+{-|
+Module      : Dhrun.AesonTypes
+Description : types
+Copyright   : (c) Valentin Reis, 2018
+License     : MIT
+Maintainer  : fre@freux.fr
+-}
+
+module Dhrun.Types.Yaml
+  ( decodeCfgFile
+  , decodeCfg
+  , encodeCfg
+  , encodeCmd
+  )
+where
+
+import qualified Dhrun.Types.Dhall                   as DT
+import           Protolude
+import           Dhall
+import           Data.Yaml
+import           Data.Aeson
+import           System.IO.Error
+
+data CheckParse = CheckParse {
+    avoids   :: Maybe [Text]
+  , wants    :: Maybe [Text]
+  } deriving (Eq, Show, Generic, ToJSON, Interpret)
+instance FromJSON CheckParse where
+    parseJSON = genericParseJSON defaultOptions { omitNothingFields  = True }
+instance FromJSON (DT.FileCheck CheckParse) where
+    parseJSON = genericParseJSON defaultOptions { omitNothingFields  = True }
+
+data Cfg = Cfg
+  { cmds      :: [Cmd],
+    workdir   :: Maybe Text,
+    verbose   :: Maybe Bool,
+    cleaning  :: Maybe Bool,
+    pre       :: Maybe [Text],
+    post      :: Maybe [Text]
+  } deriving (Eq, Show, Generic, FromJSON, ToJSON)
+
+data Cmd = Cmd {
+    name        :: Text
+  , exitcode    :: Maybe Integer
+  , args        :: Maybe [Text]
+  , vars        :: Maybe [DT.EnvVar]
+  , passvars    :: Maybe [Text]
+  , timeout     :: Maybe Natural
+  , out         :: DT.FileCheck CheckParse
+  , err         :: DT.FileCheck CheckParse
+  , postchecks  :: Maybe [DT.FileCheck CheckParse]
+  , otherwd     :: Maybe Text
+  } deriving (Eq, Show, Generic, ToJSON, Interpret)
+instance FromJSON Cmd where
+    parseJSON = genericParseJSON defaultOptions { omitNothingFields  = True }
+
+fromInternalCheck :: DT.Check -> CheckParse
+fromInternalCheck c = CheckParse {..}
+ where
+  avoids = case DT.avoids c of
+    [] -> Nothing
+    l  -> Just l
+  wants = case DT.wants c of
+    [] -> Nothing
+    l  -> Just l
+
+toInternalCheck :: CheckParse -> DT.Check
+toInternalCheck cp =
+  DT.Check {wants = fromMaybe [] (wants cp), avoids = fromMaybe [] (avoids cp)}
+
+toInternalCmd :: Cmd -> DT.Cmd
+toInternalCmd c = DT.Cmd
+  { name       = name c
+  , exitcode   = exitcode c
+  , args       = fromMaybe [] $ args c
+  , vars       = fromMaybe [] $ vars c
+  , passvars   = fromMaybe [] $ passvars c
+  , out        = toInternalCheck <$> out c
+  , err        = toInternalCheck <$> err c
+  , postchecks = case postchecks c of
+    (Just l) -> (toInternalCheck <$>) <$> l
+    Nothing  -> []
+  , timeout    = timeout c
+  , otherwd = otherwd c
+  }
+
+toInternal :: Cfg -> DT.Cfg
+toInternal d = DT.Cfg
+  { cmds     = toInternalCmd <$> cmds d
+  , verbose  = fromMaybe False (verbose d)
+  , cleaning = fromMaybe False (cleaning d)
+  , pre      = fromMaybe [] (pre d)
+  , post     = fromMaybe [] (post d)
+  , workdir  = fromMaybe "./" (workdir d)
+  }
+
+fromInternalCmd :: DT.Cmd -> Cmd
+fromInternalCmd c = Cmd {..}
+ where
+  name    = DT.name c
+  exitcode = DT.exitcode c
+  out     = fromInternalCheck <$> DT.out c
+  err     = fromInternalCheck <$> DT.err c
+  timeout = DT.timeout c
+  otherwd   = DT.otherwd c
+  args    = case DT.args c of
+    [] -> Nothing
+    l  -> Just l
+  vars = case DT.vars c of
+    [] -> Nothing
+    l  -> Just l
+  passvars = case DT.passvars c of
+    [] -> Nothing
+    l  -> Just l
+  postchecks = case DT.postchecks c of
+    [] -> Nothing
+    l  -> Just ((fromInternalCheck <$>) <$> l)
+
+fromInternal :: DT.Cfg -> Cfg
+fromInternal d = Cfg {..}
+ where
+  workdir = case DT.workdir d of
+    "./" -> Nothing
+    w    -> Just w
+  cmds     = fromInternalCmd <$> DT.cmds d
+  verbose  = if DT.verbose d then Just True else Nothing
+  cleaning = if DT.cleaning d then Just True else Nothing
+  pre      = case DT.pre d of
+    [] -> Nothing
+    l  -> Just l
+  post = case DT.post d of
+    [] -> Nothing
+    l  -> Just l
+
+decodeCfgFile :: (MonadIO m) => Text -> m DT.Cfg
+decodeCfgFile fn = liftIO $ try (decodeFileEither (toS fn)) >>= \case
+  Left  e          -> throwError e
+  Right (Left  pa) -> throwError $ userError $ "parse fail:" <> show pa
+  Right (Right a ) -> return $ toInternal a
+
+decodeCfg :: ByteString -> Either ParseException DT.Cfg
+decodeCfg fn = toInternal <$> decodeEither' fn
+
+encodeCfg :: DT.Cfg -> ByteString
+encodeCfg = Data.Yaml.encode . fromInternal
+
+encodeCmd :: DT.Cmd -> ByteString
+encodeCmd = Data.Yaml.encode . fromInternalCmd
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,100 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fno-warn-unused-imports #-}
+{-# language OverloadedStrings #-}
+{-# language ScopedTypeVariables #-}
+{-# language FlexibleContexts #-}
+{-# language FlexibleInstances #-}
+{-# language NoImplicitPrelude #-}
+
+module Main
+  ( main
+  )
+where
+
+import           Protolude
+            hiding ( (<.>) )
+import           Test.Tasty
+            hiding ( Timeout )
+import           Test.Tasty.HUnit
+import           Test.Tasty.Golden
+import           System.FilePath
+{-import           Test.Tasty.Hspec-}
+import           Test.Tasty.QuickCheck                             as QC
+{-import qualified Data.Text                     as T-}
+{-import           Data.Text.Arbitrary-}
+{-import           Control.Monad.Mock-}
+{-import           Control.Monad.Mock.TH-}
+
+import           Dhrun.Pure
+import           Dhrun.Types.Cfg
+import           Generic.Random
+import           Data.Text.Arbitrary
+
+instance Arbitrary (FileCheck Check) where
+  arbitrary = genericArbitraryU
+instance Arbitrary FileName where
+  arbitrary = genericArbitraryU
+instance Arbitrary WorkDir where
+  arbitrary = genericArbitraryU
+instance Arbitrary Check where
+  arbitrary = genericArbitraryU
+instance Arbitrary Pattern where
+  arbitrary = genericArbitraryU
+instance Arbitrary CommandName where
+  arbitrary = genericArbitraryU
+instance Arbitrary EnvVar where
+  arbitrary = genericArbitraryU
+instance Arbitrary Std where
+  arbitrary = genericArbitraryU
+instance Arbitrary VarValue where
+  arbitrary = genericArbitraryU
+instance Arbitrary VarName where
+  arbitrary = genericArbitraryU
+instance Arbitrary Arg where
+  arbitrary = genericArbitraryU
+instance Arbitrary Cmd where
+  arbitrary = genericArbitraryU
+instance Arbitrary CmdResult where
+  arbitrary = genericArbitraryU
+
+goldenYml :: FilePath -> TestTree
+goldenYml fn = testCase
+  (toS fn)
+  (do
+    ymlCfg <- decodeCfgFile (toS $ fn <.> ".yml")
+    dhCfg  <- inputCfg (toS $ "./" <> fn <.> "dh")
+    assertEqual "files not equal" ymlCfg dhCfg
+  )
+
+main :: IO ()
+main = do
+  goldenTests <-
+    testGroup "Golden tests"
+      <$> (   findByExtension [".yml"] "examples"
+          <&> fmap (goldenYml . toS . dropExtension)
+          )
+  defaultMain $ testGroup "Tests" [unitTests, goldenTests, qcProps]
+
+unitTests :: TestTree
+unitTests = testGroup
+  "HUnit tests"
+  [ testCase "Pure.envVars"
+    $   envVars [EnvVar {varname = VarName "JOBVAR", value = VarValue "JOBVAL"}]
+                [VarName "PASSME"]
+                [("PASSME", "5"), ("DISCARDME", "9")]
+    @?= [("JOBVAR", "JOBVAL"), ("PASSME", "5")]
+  ]
+
+qcProps :: TestTree
+qcProps = testGroup
+  "QuickCheck specs"
+  [ QC.testProperty "concludeCmd fail/success patterns"
+      $ \cmdresult -> shouldConclude cmdresult (concludeCmd True cmdresult)
+  ]
+ where
+  shouldConclude :: CmdResult -> Either a b -> Bool
+  shouldConclude cmdresult concluded = case cmdresult of
+    DiedLegal{}    -> True
+    FoundAll{}     -> isRight concluded
+    DiedExpected{} -> isRight concluded
+    _              -> isLeft concluded
