diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
 # Intro
 
-A neovim plugin that provides project-specific configuration file loading.
+A neovim plugin that provides project-specific configuration file loading and performs runtime tasks on projects.
+
+# Install
+
+Use the [chromatin] plugin manager with the spec `hackage:proteome`.
+
+[chromatin]: https://github.com/tek/chromatin.nvim
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,6 @@
+import Neovim
+
+import Proteome.Plugin (plugin)
+
+main :: IO ()
+main = neovim defaultConfig {plugins = [plugin]}
diff --git a/lib/Proteome/Config.hs b/lib/Proteome/Config.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Config.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Proteome.Config(
+  proReadConfig,
+  readConfig,
+  ProjectConfig (..),
+)
+where
+
+import GHC.Generics (Generic)
+import Control.DeepSeq (NFData)
+import qualified Data.Map as Map
+import Data.Foldable (traverse_)
+import Data.Map.Strict (Map)
+import Neovim (NvimObject, toObject, Dictionary, Object, vim_command')
+import Ribosome.Data.Ribo (Ribo)
+import qualified Ribosome.Data.Ribo as Ribo (inspect)
+import qualified Proteome.Data.Env as Env (mainProject)
+import Proteome.Data.Project (
+  Project(Project, types),
+  ProjectType(ProjectType, projectType),
+  ProjectName(ProjectName),
+  ProjectMetadata(DirProject),
+  )
+import Proteome.Data.Proteome
+
+newtype ProjectConfig =
+  ProjectConfig {
+    projectTypes :: Map ProjectType [FilePath]
+  }
+  deriving (Generic, NFData)
+
+instance NvimObject ProjectConfig where
+  toObject ProjectConfig {..} =
+    (toObject :: Dictionary -> Object) . Map.fromList $
+    [
+      ("projectTypes", toObject projectTypes)
+    ]
+
+runtime :: FilePath -> Ribo a ()
+runtime path = vim_command' $ "runtime " ++ path ++ ".vim"
+
+runtimeConf :: FilePath -> String -> Ribo a ()
+runtimeConf confDir path = runtime(confDir ++ "/" ++ path)
+
+typeProjectConf :: FilePath -> ProjectName -> ProjectType -> Ribo a ()
+typeProjectConf confDir (ProjectName name') (ProjectType tpe') = do
+  runtimeConf confDir tpe'
+  runtimeConf confDir $ tpe' ++ "/" ++ name'
+
+readConfigMeta :: String -> Project -> Ribo a ()
+readConfigMeta confDir (Project (DirProject name' _ tpe') _ _ _) =
+  traverse_ (typeProjectConf confDir name') tpe'
+readConfigMeta _ _ = return ()
+
+readConfigProject :: String -> Project -> Ribo a ()
+readConfigProject confDir project = do
+  readConfigMeta confDir project
+  traverse_ (runtimeConf confDir) (fmap projectType (types project))
+
+readConfig :: String -> Project -> Ribo a ()
+readConfig confDir project = do
+  runtimeConf confDir "all/*"
+  readConfigProject confDir project
+
+proReadConfig :: Proteome ()
+proReadConfig = do
+  main <- Ribo.inspect Env.mainProject
+  readConfig "project" main
diff --git a/lib/Proteome/Data/Env.hs b/lib/Proteome/Data/Env.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Data/Env.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Proteome.Data.Env (
+  Env(..),
+  _mainProject,
+  _projects,
+  _errors,
+) where
+
+import Control.Lens (makeClassy_)
+import Data.Default.Class (Default(def))
+import Ribosome.Data.Errors (Errors)
+import Proteome.Data.Project
+
+data Env = Env {
+  mainProject :: Project,
+  projects :: [Project],
+  errors :: Errors
+}
+
+makeClassy_ ''Env
+
+instance Default Env where
+  def = Env def def def
diff --git a/lib/Proteome/Data/Project.hs b/lib/Proteome/Data/Project.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Data/Project.hs
@@ -0,0 +1,86 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Proteome.Data.Project(
+  ProjectMetadata (..),
+  Project (..),
+  ProjectType (..),
+  ProjectLang (..),
+  ProjectRoot (..),
+  ProjectName (..),
+  _meta,
+  _lang,
+  langOrType,
+) where
+
+import GHC.Generics (Generic)
+import Control.Lens (makeClassy_)
+import Control.DeepSeq
+import Data.Default.Class (Default(def))
+import Neovim.Classes (NvimObject(..))
+import Ribosome.Internal.NvimObject (deriveString)
+
+newtype ProjectName = ProjectName String
+  deriving (Eq, Show, Generic, NFData)
+
+instance NvimObject ProjectName where
+  toObject (ProjectName s) = toObject s
+  fromObject = deriveString ProjectName
+
+newtype ProjectType =
+  ProjectType {
+    projectType :: String
+  }
+  deriving (Ord, Eq, Show, Generic, NFData)
+
+instance NvimObject ProjectType where
+  toObject (ProjectType s) = toObject s
+  fromObject = deriveString ProjectType
+
+newtype ProjectLang = ProjectLang String
+  deriving (Eq, Show, Generic, NFData)
+
+instance NvimObject ProjectLang where
+  toObject (ProjectLang s) = toObject s
+  fromObject = deriveString ProjectLang
+
+newtype ProjectRoot = ProjectRoot FilePath
+  deriving (Eq, Show, Generic, NFData)
+
+instance NvimObject ProjectRoot where
+  toObject (ProjectRoot s) = toObject s
+  fromObject = deriveString ProjectRoot
+
+data ProjectMetadata =
+  DirProject {
+    name :: ProjectName,
+    root :: ProjectRoot,
+    tpe :: Maybe ProjectType
+  }
+  | VirtualProject {
+    name :: ProjectName
+  }
+  deriving (Eq, Show)
+
+instance Default ProjectMetadata where
+  def = VirtualProject (ProjectName "main")
+
+data Project =
+  Project {
+    meta :: ProjectMetadata,
+    types :: [ProjectType],
+    lang :: Maybe ProjectLang,
+    langs :: [ProjectLang]
+  }
+  deriving (Eq, Show)
+
+makeClassy_ ''Project
+
+instance Default Project where
+  def = Project def def def def
+
+langOrType :: Maybe ProjectLang -> Maybe ProjectType -> Maybe ProjectLang
+langOrType (Just lang') _ = Just lang'
+langOrType Nothing (Just (ProjectType tpe')) = Just (ProjectLang tpe')
+langOrType _ _ = Nothing
diff --git a/lib/Proteome/Data/ProjectSpec.hs b/lib/Proteome/Data/ProjectSpec.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Data/ProjectSpec.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Proteome.Data.ProjectSpec(
+  ProjectSpec (..),
+) where
+
+import GHC.Generics (Generic)
+import Control.DeepSeq (NFData)
+import qualified Data.Map as Map
+import Data.Maybe (catMaybes)
+import Data.MessagePack
+import Neovim.Classes (NvimObject(..), Dictionary)
+import Proteome.Data.Project (ProjectName, ProjectType, ProjectLang, ProjectRoot)
+
+data ProjectSpec =
+  ProjectSpec {
+    name :: ProjectName,
+    root :: ProjectRoot,
+    tpe :: Maybe ProjectType,
+    types :: [ProjectType],
+    lang :: Maybe ProjectLang,
+    langs :: [ProjectLang]
+  }
+  deriving (Generic, NFData)
+
+instance NvimObject ProjectSpec where
+  toObject ProjectSpec {..} =
+    (toObject :: Dictionary -> Object) . Map.fromList $
+    [
+      ("name", toObject name),
+      ("root", toObject root),
+      ("types", toObject types),
+      ("langs", toObject langs)
+    ] ++ catMaybes
+    [
+      tpe >>= \a -> return ("tpe", toObject a),
+      lang >>= \a -> return ("lang", toObject a)
+    ]
diff --git a/lib/Proteome/Data/Proteome.hs b/lib/Proteome/Data/Proteome.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Data/Proteome.hs
@@ -0,0 +1,9 @@
+module Proteome.Data.Proteome(
+  Proteome
+) where
+
+import UnliftIO.STM (TVar)
+import Ribosome.Data.Ribo (Ribo)
+import Proteome.Data.Env (Env)
+
+type Proteome a = Ribo (TVar Env) a
diff --git a/lib/Proteome/Init.hs b/lib/Proteome/Init.hs
--- a/lib/Proteome/Init.hs
+++ b/lib/Proteome/Init.hs
@@ -1,15 +1,85 @@
-{-# LANGUAGE TemplateHaskell #-}
-
-module Proteome.Init
-(
+module Proteome.Init(
   initialize,
-  env
+  proteomeStage2,
+  proteomeStage4,
 ) where
 
-import Neovim
+import Control.Monad.Reader
+import Data.Default.Class (Default(def))
+import System.Directory (getCurrentDirectory, makeAbsolute)
+import System.FilePath (takeFileName, takeDirectory)
+import System.Log.Logger (updateGlobalLogger, setLevel, Priority(ERROR))
+import Control.Monad.IO.Class (MonadIO)
+import Neovim.Context.Internal (Neovim)
+import UnliftIO.STM (TVar, newTVarIO)
+import Ribosome.Config.Settings (setting, settingE, updateSetting)
+import Ribosome.Data.Ribo
+import Ribosome.Data.Ribosome (Ribosome(Ribosome))
+import Ribosome.Internal.IO (retypeNeovim)
+import Proteome.Data.Env (Env(Env))
+import Proteome.Data.Proteome
+import Proteome.Data.Project (
+  Project(meta),
+  ProjectName(..),
+  ProjectRoot(..),
+  ProjectType(..),
+  ProjectMetadata(DirProject),
+  )
+import Proteome.Project.Resolve (resolveProject)
+import qualified Proteome.Settings as S
 
-env :: Neovim startupEnv ()
-env = return ()
+pathData :: MonadIO m => Either String FilePath -> m (ProjectRoot, ProjectName, ProjectType)
+pathData override = do
+  cwd <- liftIO $ either (const getCurrentDirectory) pure override
+  absMainDir <- liftIO $ makeAbsolute cwd
+  return (
+    ProjectRoot absMainDir,
+    ProjectName $ takeFileName absMainDir,
+    ProjectType $ (takeFileName . takeDirectory) absMainDir
+    )
 
-initialize :: Neovim startupEnv ()
-initialize = return ()
+mainProject :: Ribo e Project
+mainProject = do
+  mainDir <- settingE S.mainProjectDir
+  (root, name, tpe) <- pathData mainDir
+  baseDirs <- setting S.projectBaseDirs
+  -- typeDirs <- setting S.projectTypeDirs
+  explicit <- setting S.projects
+  config <- setting S.projectConfig
+  return $ resolveProject baseDirs explicit config root name (Just tpe)
+
+loadPersistedBuffers :: Project -> Neovim e ()
+loadPersistedBuffers _ = return ()
+
+updateMainType :: Maybe ProjectType -> Ribo e ()
+updateMainType (Just tpe) = updateSetting S.mainType tpe
+updateMainType Nothing = return ()
+
+setProjectVars :: ProjectMetadata -> Ribo e ()
+setProjectVars (DirProject name _ tpe) = do
+  updateSetting S.mainName name
+  updateMainType tpe
+setProjectVars _ = return ()
+
+initWithMain :: Project -> Ribo e Env
+initWithMain main = do
+  loadPersistedBuffers main
+  setProjectVars (meta main)
+  return $ Env main [] def
+
+initialize' :: Ribo e Env
+initialize' = do
+  main <- mainProject
+  initWithMain main
+
+initialize :: Neovim e (TVar Env)
+initialize = do
+  liftIO $ updateGlobalLogger "Neovim.Plugin" (setLevel ERROR)
+  env <- retypeNeovim (Ribosome "proteome") initialize'
+  newTVarIO env
+
+proteomeStage2 :: Proteome ()
+proteomeStage2 = return ()
+
+proteomeStage4 :: Proteome ()
+proteomeStage4 = return ()
diff --git a/lib/Proteome/Log.hs b/lib/Proteome/Log.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Log.hs
@@ -0,0 +1,21 @@
+module Proteome.Log(
+  debug,
+  info,
+  debugS,
+  infoS,
+) where
+
+import Ribosome.Data.Ribo (Ribo)
+import qualified Ribosome.Log as R (debug, info)
+
+debug :: String -> Ribo e ()
+debug = R.debug "proteome"
+
+info :: String -> Ribo e ()
+info = R.info "proteome"
+
+debugS :: Show a => a -> Ribo e ()
+debugS = R.debug "proteome"
+
+infoS :: Show a => a -> Ribo e ()
+infoS = R.info "proteome"
diff --git a/lib/Proteome/Plugin.hs b/lib/Proteome/Plugin.hs
--- a/lib/Proteome/Plugin.hs
+++ b/lib/Proteome/Plugin.hs
@@ -1,23 +1,36 @@
 {-# LANGUAGE TemplateHaskell #-}
 
 module Proteome.Plugin(
-  plugin
+  plugin,
 )
 where
 
+import UnliftIO.STM (TVar)
 import Neovim
-
-import Proteome.Init (initialize, env)
+import Ribosome.Data.Ribosome (Ribosome(Ribosome))
+import Proteome.Init
+import Proteome.Data.Env
 import Proteome.Add (proAdd, proteomePoll)
+import Proteome.Config (proReadConfig)
+import Proteome.Tags (proTags)
+import Proteome.Save (proSave)
 
+plugin' :: TVar Env -> Plugin (Ribosome (TVar Env))
+plugin' env =
+  Plugin {
+    environment = Ribosome "proteome" env,
+    exports = [
+      $(function' 'proteomePoll) Sync,
+      $(function' 'proteomeStage2) Async,
+      $(function' 'proteomeStage4) Async,
+      $(function' 'proAdd) Async,
+      $(function' 'proSave) Async,
+      $(function' 'proTags) Async,
+      $(function' 'proReadConfig) Sync
+    ]
+  }
+
 plugin :: Neovim (StartupConfig NeovimConfig) NeovimPlugin
 plugin = do
-  initialize
-  wrapPlugin
-    Plugin {
-      environment = env,
-      exports = [
-        $(function' 'proteomePoll) Sync,
-        $(function' 'proAdd) Async
-      ]
-    }
+  env <- initialize
+  wrapPlugin $ plugin' env
diff --git a/lib/Proteome/Project/Resolve.hs b/lib/Proteome/Project/Resolve.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Project/Resolve.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE NamedFieldPuns #-}
+
+module Proteome.Project.Resolve(
+  resolveProject,
+) where
+
+import Data.List (find)
+import Data.Maybe (fromMaybe)
+import System.FilePath (takeDirectory)
+import Ribosome.Data.Maybe (orElse)
+import Proteome.Config (ProjectConfig)
+import Proteome.Data.Project (
+  Project(Project),
+  ProjectName(..),
+  ProjectRoot(..),
+  ProjectType(..),
+  ProjectLang(..),
+  ProjectMetadata(DirProject, VirtualProject),
+  )
+import Proteome.Data.ProjectSpec (ProjectSpec(ProjectSpec))
+import qualified Proteome.Data.ProjectSpec as PS (ProjectSpec(..))
+
+projectFromSegments :: ProjectType -> ProjectName -> ProjectRoot -> Project
+projectFromSegments tpe name root =
+  Project (DirProject name root (Just tpe)) [] (Just (ProjectLang (projectType tpe))) []
+
+projectFromSpec :: ProjectSpec -> Project
+projectFromSpec (ProjectSpec name root tpe types lang langs) =
+  Project (DirProject name root tpe) types lang langs
+
+hasProjectRoot :: ProjectRoot -> ProjectSpec -> Bool
+hasProjectRoot root spec = root == PS.root spec
+
+hasProjectTypeName :: ProjectType -> ProjectName -> ProjectSpec -> Bool
+hasProjectTypeName tpe' name' (ProjectSpec name _ (Just tpe) _ _ _) =
+  name' == name && tpe' == tpe
+hasProjectTypeName _ _ _ = False
+
+byProjectTypeName :: [ProjectSpec] -> ProjectName -> ProjectType -> Maybe ProjectSpec
+byProjectTypeName specs name tpe = find (hasProjectTypeName tpe name) specs
+
+byProjectBases :: [FilePath] -> ProjectRoot -> Bool
+byProjectBases baseDirs (ProjectRoot root) = elem ((takeDirectory . takeDirectory) root) baseDirs
+
+virtualProject :: ProjectName -> Project
+virtualProject name = Project (VirtualProject name) [] Nothing []
+
+resolveByType :: [FilePath] -> [ProjectSpec] -> ProjectRoot -> ProjectName -> ProjectType -> Maybe Project
+resolveByType baseDirs explicit root name tpe =
+  orElse (if byPath then Just (projectFromSegments tpe name root) else Nothing) (fmap projectFromSpec byTypeName)
+  where
+    byTypeName = byProjectTypeName explicit name tpe
+    byPath = byProjectBases baseDirs root
+
+resolveByRoot :: [ProjectSpec] -> ProjectRoot -> Maybe Project
+resolveByRoot explicit root =
+  fmap projectFromSpec byRoot
+  where
+    byRoot = find (hasProjectRoot root) explicit
+
+resolveProject ::
+  [FilePath] ->
+  [ProjectSpec] ->
+  ProjectConfig ->
+  ProjectRoot ->
+  ProjectName ->
+  Maybe ProjectType ->
+  Project
+resolveProject baseDirs explicit _ root name tpe =
+  fromMaybe byTypeOrVirtual byRoot
+  where
+    byTypeOrVirtual = fromMaybe (virtualProject name) byType
+    byType = tpe >>= resolveByType baseDirs explicit root name
+    byRoot = resolveByRoot explicit root
diff --git a/lib/Proteome/Save.hs b/lib/Proteome/Save.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Save.hs
@@ -0,0 +1,8 @@
+module Proteome.Save(
+  proSave,
+) where
+
+import Proteome.Data.Proteome (Proteome)
+
+proSave :: Proteome ()
+proSave = return ()
diff --git a/lib/Proteome/Settings.hs b/lib/Proteome/Settings.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Settings.hs
@@ -0,0 +1,52 @@
+module Proteome.Settings(
+  mainProjectDir,
+  projects,
+  projectTypeDirs,
+  projectBaseDirs,
+  projectConfig,
+  mainName,
+  mainType,
+  tagsCommand,
+  tagsArgs,
+  tagsFork,
+  tagsFileName,
+) where
+
+import qualified Data.Map as Map
+import Ribosome.Config.Settings
+import Proteome.Data.Project (ProjectName, ProjectType)
+import Proteome.Data.ProjectSpec (ProjectSpec)
+import Proteome.Config
+
+mainProjectDir :: Setting String
+mainProjectDir = Setting "main_project_dir" True Nothing
+
+projects :: Setting [ProjectSpec]
+projects = Setting "projects" True (Just [])
+
+projectBaseDirs :: Setting [FilePath]
+projectBaseDirs = Setting "project_base_dirs" True (Just [])
+
+projectTypeDirs :: Setting [FilePath]
+projectTypeDirs = Setting "project_type_dirs" True (Just [])
+
+projectConfig :: Setting ProjectConfig
+projectConfig = Setting "project_config" True (Just (ProjectConfig Map.empty))
+
+mainName :: Setting ProjectName
+mainName = Setting "main_name" True Nothing
+
+mainType :: Setting ProjectType
+mainType = Setting "main_type" True Nothing
+
+tagsCommand :: Setting String
+tagsCommand = Setting "tags_command" True (Just "ctags")
+
+tagsArgs :: Setting String
+tagsArgs = Setting "tags_args" True (Just "-R --languages={langsComma} -f {tagFile} {root}")
+
+tagsFork :: Setting Bool
+tagsFork = Setting "tags_fork" True (Just True)
+
+tagsFileName :: Setting FilePath
+tagsFileName = Setting "tags_file_name" True (Just ".tags")
diff --git a/lib/Proteome/Tags.hs b/lib/Proteome/Tags.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Tags.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Proteome.Tags(
+  proTags,
+) where
+
+import GHC.IO.Exception (ExitCode(..))
+import Control.Monad (when)
+import Control.Monad.IO.Class (liftIO)
+import Control.Lens (over)
+import Data.List (intercalate)
+import qualified Data.Map.Strict as Map (adjust)
+import Data.Maybe (maybeToList)
+import Data.String.Utils (replace)
+import System.Process (readProcessWithExitCode)
+import System.FilePath ((</>))
+import System.Directory (setCurrentDirectory, doesFileExist, removePathForcibly)
+import Ribosome.Config.Settings (setting)
+import qualified Ribosome.Data.Ribo as Ribo (inspect, modify)
+import Ribosome.Data.Errors (Errors(Errors), Error(Error), ComponentName(ComponentName))
+import Ribosome.Internal.IO (forkNeovim)
+import Proteome.Data.Env (Env(mainProject))
+import qualified Proteome.Data.Env as Env (_errors)
+import Proteome.Data.Proteome (Proteome)
+import Proteome.Data.Project (
+  Project (Project),
+  ProjectLang(ProjectLang),
+  ProjectRoot(ProjectRoot),
+  ProjectMetadata (DirProject),
+  langOrType,
+  )
+import qualified Proteome.Settings as S (tagsCommand, tagsArgs, tagsFork, tagsFileName)
+
+replaceFormatItem :: String -> (String, String) -> String
+replaceFormatItem original (placeholder, replacement) =
+  replace ("{" ++ placeholder ++ "}") replacement original
+
+formatTagsArgs :: [ProjectLang] -> ProjectRoot -> FilePath -> String -> String
+formatTagsArgs langs (ProjectRoot root) fileName formatString =
+  foldl replaceFormatItem formatString formats
+  where
+    formats = [
+      ("langsComma", intercalate "," $ fmap (\(ProjectLang l) -> l) langs),
+      ("tagFile", fileName),
+      ("root", root)
+      ]
+
+deleteTags :: ProjectRoot -> Proteome ()
+deleteTags (ProjectRoot root) = do
+  name <- setting S.tagsFileName
+  let path = root </> name
+  exists <- liftIO $ doesFileExist path
+  when exists $ liftIO $ removePathForcibly path
+
+storeError :: ComponentName -> [String] -> Errors -> Errors
+storeError name msg (Errors errors) =
+  Errors (Map.adjust (err:) name errors)
+  where
+    err = Error time msg
+    time = 0
+
+notifyError :: String -> Proteome ()
+notifyError e =
+  Ribo.modify $ over Env._errors (storeError (ComponentName "ctags") [e])
+
+executeTags :: ProjectRoot -> String -> String -> Proteome ()
+executeTags (ProjectRoot root) cmd args = do
+  liftIO $ setCurrentDirectory root
+  deleteTags (ProjectRoot root)
+  (exitcode, _, stderr) <- liftIO $ readProcessWithExitCode cmd [args] ""
+  case exitcode of
+    ExitSuccess -> return ()
+    ExitFailure _ -> notifyError stderr
+
+regenerateTags :: ProjectRoot -> [ProjectLang] -> Proteome ()
+regenerateTags root langs = do
+  cmd <- setting S.tagsCommand
+  args <- setting S.tagsArgs
+  fileName <- setting S.tagsFileName
+  let thunk = executeTags root cmd (formatTagsArgs langs root fileName args)
+  fork <- setting S.tagsFork
+  _ <- if fork then forkNeovim thunk else thunk
+  return ()
+
+proTags :: Proteome ()
+proTags = do
+  main <- Ribo.inspect mainProject
+  case main of
+    Project (DirProject _ root tpe) _ lang langs ->
+      regenerateTags root (maybeToList (langOrType lang tpe) ++ langs)
+    _ -> return ()
diff --git a/lib/Proteome/Test/Config.hs b/lib/Proteome/Test/Config.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Test/Config.hs
@@ -0,0 +1,13 @@
+module Proteome.Test.Config(
+  defaultTestConfigWith,
+  defaultTestConfig,
+) where
+
+import Ribosome.Test.Embed (TestConfig, Vars)
+import qualified Ribosome.Test.Embed as E (defaultTestConfig, defaultTestConfigWith)
+
+defaultTestConfigWith :: Vars -> TestConfig
+defaultTestConfigWith = E.defaultTestConfigWith "proteome"
+
+defaultTestConfig :: TestConfig
+defaultTestConfig = E.defaultTestConfig "proteome"
diff --git a/lib/Proteome/Test/Functional.hs b/lib/Proteome/Test/Functional.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Test/Functional.hs
@@ -0,0 +1,15 @@
+module Proteome.Test.Functional(
+  spec,
+  specWith,
+) where
+
+import Ribosome.Data.Ribo (Ribo)
+import Ribosome.Test.Embed (Vars)
+import Ribosome.Test.Functional (functionalSpec)
+import Proteome.Test.Config (defaultTestConfig, defaultTestConfigWith)
+
+spec :: Ribo () () -> IO ()
+spec = functionalSpec defaultTestConfig
+
+specWith :: Vars -> Ribo () () -> IO ()
+specWith vars = functionalSpec $ defaultTestConfigWith vars
diff --git a/lib/Proteome/Test/Unit.hs b/lib/Proteome/Test/Unit.hs
new file mode 100644
--- /dev/null
+++ b/lib/Proteome/Test/Unit.hs
@@ -0,0 +1,27 @@
+module Proteome.Test.Unit(
+  spec,
+  specWith,
+  specWithDef,
+) where
+
+import Data.Default.Class (def)
+import UnliftIO.STM (newTVarIO)
+import Ribosome.Test.Embed (Vars)
+import Ribosome.Test.Unit (unitSpec)
+import Proteome.Data.Proteome (Proteome)
+import Proteome.Data.Env (Env)
+import Proteome.Test.Config (defaultTestConfig, defaultTestConfigWith)
+
+spec :: Env -> Proteome () -> IO ()
+spec e s = do
+  t <- newTVarIO e
+  unitSpec defaultTestConfig t s
+
+specWith :: Env -> Proteome () -> Vars -> IO ()
+specWith e s vars = do
+  t <- newTVarIO e
+  unitSpec (defaultTestConfigWith vars) t s
+
+specWithDef :: Proteome () -> Vars -> IO ()
+specWithDef s v = do
+  specWith def s v
diff --git a/lib/Ribosome/Api/Option.hs b/lib/Ribosome/Api/Option.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Option.hs
@@ -0,0 +1,14 @@
+module Ribosome.Api.Option(
+  optionCat,
+  rtpCat
+) where
+
+import Neovim
+
+optionCat :: String -> String -> Neovim env ()
+optionCat name extra = do
+  current <- vim_get_option' name >>= fromObject'
+  vim_set_option' name $ toObject $ current ++ "," ++ extra
+
+rtpCat :: String -> Neovim env ()
+rtpCat = optionCat "runtimepath"
diff --git a/lib/Ribosome/Api/Response.hs b/lib/Ribosome/Api/Response.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Response.hs
@@ -0,0 +1,27 @@
+module Ribosome.Api.Response(
+  nvimFatal,
+  nvimResponseString,
+  nvimResponseStringArray,
+  nvimValidateFatal
+)
+where
+
+import Data.ByteString.UTF8
+import Neovim
+
+nvimFatal :: Either NeovimException a -> Neovim env a
+nvimFatal (Right a) = return a
+nvimFatal (Left e) = (liftIO . fail . show) e
+
+nvimResponseString :: Object -> Neovim env String
+nvimResponseString (ObjectString a) = return $ toString a
+nvimResponseString a = liftIO . fail $ "invalid nvim type for String: " ++ (show a)
+
+nvimResponseStringArray :: Object -> Neovim env [String]
+nvimResponseStringArray (ObjectArray a) = traverse nvimResponseString a
+nvimResponseStringArray a = liftIO . fail $ "invalid nvim type for Array: " ++ (show a)
+
+nvimValidateFatal :: (Object -> Neovim env a) -> (Either NeovimException Object) -> Neovim env a
+nvimValidateFatal validate response = do
+  result <- nvimFatal response
+  validate result
diff --git a/lib/Ribosome/Config/Settings.hs b/lib/Ribosome/Config/Settings.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Config/Settings.hs
@@ -0,0 +1,47 @@
+module Ribosome.Config.Settings(
+  Setting (..),
+  setting,
+  settingE,
+  updateSetting,
+  settingVariableName,
+) where
+
+import Neovim
+import Ribosome.Data.Ribo (Ribo)
+import qualified Ribosome.Data.Ribosome as R (name)
+
+data Setting a =
+  Setting {
+    name :: String,
+    prefix :: Bool,
+    fallback :: Maybe a
+  }
+
+settingVariableName :: Setting a -> Ribo e String
+settingVariableName (Setting n False _) = return n
+settingVariableName (Setting n True _) = do
+  pluginName <- fmap R.name $ ask
+  return $ pluginName ++ "_" ++ n
+
+settingE :: NvimObject a => Setting a -> Ribo e (Either String a)
+settingE s @ (Setting _ _ fallback') = do
+  varName <- settingVariableName s
+  raw <- vim_get_var varName
+  case raw of
+    Right o -> fromObject' o
+    Left a -> return $ case fallback' of
+      Just fb -> Right fb
+      Nothing -> Left $ show a
+
+setting :: NvimObject a => Setting a -> Ribo e a
+setting s = do
+  raw <- settingE s
+  case raw of
+    Right o -> return o
+    Left e -> fail e
+
+updateSetting :: NvimObject a => Setting a -> a -> Ribo e ()
+updateSetting s a = do
+  varName <- settingVariableName s
+  _ <- vim_set_var' varName (toObject a)
+  return ()
diff --git a/lib/Ribosome/Data/Errors.hs b/lib/Ribosome/Data/Errors.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Errors.hs
@@ -0,0 +1,25 @@
+module Ribosome.Data.Errors(
+  ComponentName(..),
+  Error(..),
+  Errors(..),
+) where
+
+import qualified Data.Map as Map
+import Data.Default.Class (Default(def))
+import Data.Map.Strict (Map)
+
+newtype ComponentName =
+  ComponentName String
+  deriving (Eq, Ord)
+
+data Error =
+  Error {
+    errorTimestamp :: Int,
+    errorMessage :: [String]
+  }
+
+newtype Errors =
+  Errors (Map ComponentName [Error])
+
+instance Default Errors where
+  def = Errors Map.empty
diff --git a/lib/Ribosome/Data/Maybe.hs b/lib/Ribosome/Data/Maybe.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Maybe.hs
@@ -0,0 +1,7 @@
+module Ribosome.Data.Maybe(
+  orElse,
+) where
+
+orElse :: Maybe a -> Maybe a -> Maybe a
+orElse fallback Nothing = fallback
+orElse _ a = a
diff --git a/lib/Ribosome/Data/Ribo.hs b/lib/Ribosome/Data/Ribo.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Ribo.hs
@@ -0,0 +1,26 @@
+module Ribosome.Data.Ribo(
+  Ribo,
+  state,
+  inspect,
+  modify,
+) where
+
+import Control.Concurrent.STM.TVar (modifyTVar)
+import UnliftIO.STM (TVar, atomically, readTVarIO)
+import Neovim (Neovim, ask)
+import Ribosome.Data.Ribosome
+
+type Ribo e = Neovim (Ribosome e)
+
+state :: Ribo (TVar e) e
+state = do
+  Ribosome _ t <- ask
+  readTVarIO t
+
+inspect :: (e -> a) -> Ribo (TVar e) a
+inspect f = fmap f state
+
+modify :: (e -> e) -> Ribo (TVar e) ()
+modify f = do
+  Ribosome _ t <- ask
+  atomically $ modifyTVar t f
diff --git a/lib/Ribosome/Data/Ribosome.hs b/lib/Ribosome/Data/Ribosome.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Ribosome.hs
@@ -0,0 +1,18 @@
+module Ribosome.Data.Ribosome(
+  Ribosome (..),
+  newRibosome,
+) where
+
+import UnliftIO.STM (TVar, newTVarIO)
+import Control.Monad.IO.Class (MonadIO)
+
+data Ribosome e =
+  Ribosome {
+    name :: String,
+    env :: e
+  }
+
+newRibosome :: MonadIO m => String -> e -> m (Ribosome (TVar e))
+newRibosome name' env' = do
+  tv <- newTVarIO env'
+  return $ Ribosome name' tv
diff --git a/lib/Ribosome/Internal/IO.hs b/lib/Ribosome/Internal/IO.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Internal/IO.hs
@@ -0,0 +1,25 @@
+module Ribosome.Internal.IO(
+  retypeNeovim,
+  forkNeovim,
+) where
+
+import GHC.Conc.Sync (forkIO)
+import Control.Monad.IO.Class (liftIO)
+import Control.Monad.Reader (ask, runReaderT, withReaderT)
+import Control.Monad.Trans.Resource (runResourceT)
+import Data.Functor (void)
+import Neovim.Context.Internal (Neovim(..), Config(..), retypeConfig, runNeovim)
+
+-- try using Contravariant with this
+retypeNeovim :: (e0 -> e1) -> Neovim e1 a -> Neovim e0 a
+retypeNeovim transform thunk = do
+  env <- Neovim ask
+  liftIO $ runReaderT (withReaderT (newEnv env) $ runResourceT $ unNeovim thunk) env
+  where
+    newEnv = retypeConfig . transform . customConfig
+
+forkNeovim :: Neovim e () -> Neovim e ()
+forkNeovim thunk = do
+  env <- Neovim ask
+  _ <- liftIO $ forkIO $ void $ runNeovim env thunk
+  return ()
diff --git a/lib/Ribosome/Internal/NvimObject.hs b/lib/Ribosome/Internal/NvimObject.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Internal/NvimObject.hs
@@ -0,0 +1,8 @@
+module Ribosome.Internal.NvimObject(
+  deriveString,
+) where
+
+import Neovim (Object, Doc, AnsiStyle, fromObject)
+
+deriveString :: (String -> a) -> Object -> Either (Doc AnsiStyle) a
+deriveString cons o = fmap cons ((fromObject o) :: Either (Doc AnsiStyle) String)
diff --git a/lib/Ribosome/Log.hs b/lib/Ribosome/Log.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Log.hs
@@ -0,0 +1,14 @@
+module Ribosome.Log(
+  debug,
+  info,
+) where
+
+import Control.Monad.IO.Class (liftIO)
+import Neovim.Log (debugM, infoM)
+import Ribosome.Data.Ribo (Ribo)
+
+debug :: Show a => String -> a -> Ribo e ()
+debug name message = liftIO $ debugM name $ show message
+
+info :: Show a => String -> a -> Ribo e ()
+info name message = liftIO $ infoM name $ show message
diff --git a/lib/Ribosome/Test/Embed.hs b/lib/Ribosome/Test/Embed.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Embed.hs
@@ -0,0 +1,47 @@
+module Ribosome.Test.Embed(
+  defaultTestConfig,
+  defaultTestConfigWith,
+  TestConfig (..),
+  Vars(..),
+  unsafeEmbeddedSpec,
+  setVars,
+  setupPluginEnv,
+) where
+
+import Data.Foldable (traverse_)
+import Neovim (Neovim, Object, vim_set_var')
+import Neovim.Test (testWithEmbeddedNeovim, Seconds(..))
+import Ribosome.Data.Ribo (Ribo)
+import Ribosome.Data.Ribosome (Ribosome(Ribosome))
+import Ribosome.Api.Option (rtpCat)
+
+type Runner env = TestConfig -> Neovim env () -> Neovim env ()
+
+newtype Vars = Vars [(String, Object)]
+
+data TestConfig =
+  TestConfig {
+    pluginName :: String,
+    extraRtp :: String,
+    logPath :: FilePath,
+    variables :: Vars
+  }
+
+defaultTestConfigWith :: String -> Vars -> TestConfig
+defaultTestConfigWith name = TestConfig name "test/f/fixtures/rtp" "test/f/temp/log"
+
+defaultTestConfig :: String -> TestConfig
+defaultTestConfig name = defaultTestConfigWith name (Vars [])
+
+setVars :: Vars -> Neovim e ()
+setVars (Vars vars) =
+  traverse_ (uncurry vim_set_var') vars
+
+setupPluginEnv :: TestConfig -> Neovim e ()
+setupPluginEnv (TestConfig _ rtp _ vars) = do
+  rtpCat rtp
+  setVars vars
+
+unsafeEmbeddedSpec :: Runner (Ribosome e) -> TestConfig -> e -> Ribo e () -> IO ()
+unsafeEmbeddedSpec runner conf env spec =
+  testWithEmbeddedNeovim Nothing (Seconds 5) (Ribosome (pluginName conf) env) $ runner conf spec
diff --git a/lib/Ribosome/Test/Exists.hs b/lib/Ribosome/Test/Exists.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Exists.hs
@@ -0,0 +1,44 @@
+module Ribosome.Test.Exists(
+  waitForPlugin,
+  sleep,
+) where
+
+import Data.Time.Clock.POSIX
+import Data.Strings (strCapitalize)
+import Control.Monad.IO.Class
+import Control.Concurrent (threadDelay)
+import Neovim
+
+epochSeconds :: MonadIO f => f Int
+epochSeconds = liftIO $ fmap round getPOSIXTime
+
+sleep :: MonadIO f => Double -> f ()
+sleep seconds = liftIO $ threadDelay $ round $ seconds * 10e6
+
+retry :: MonadIO f => Double -> Int -> f a -> (a -> f (Either String b)) -> f b
+retry interval timeout thunk check = do
+  start <- epochSeconds
+  step start
+  where
+    step start = do
+      result <- thunk
+      checked <- check result
+      recurse start checked
+    recurse _ (Right b) = return b
+    recurse start (Left e) = do
+      current <- epochSeconds
+      if (current - start) < timeout
+      then do
+        sleep interval
+        step start
+      else fail e
+
+waitForPlugin :: String -> Double -> Int -> Neovim env ()
+waitForPlugin name interval timeout = do
+  retry interval timeout thunk check
+  where
+    thunk = vim_call_function "exists" [toObject $ "*" ++ (strCapitalize name) ++ "Poll"]
+    check (Right (ObjectInt 1)) = return $ Right ()
+    check (Right a) = return $ Left $ errormsg ++ "weird return type " ++ (show a)
+    check (Left e) = return $ Left $ errormsg ++ (show e)
+    errormsg = "plugin didn't start within " ++ (show timeout) ++ " seconds: "
diff --git a/lib/Ribosome/Test/File.hs b/lib/Ribosome/Test/File.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/File.hs
@@ -0,0 +1,27 @@
+module Ribosome.Test.File(
+  tempDirIO,
+  tempDir,
+) where
+
+import Control.Monad.IO.Class (liftIO)
+import Neovim (Neovim)
+import System.Directory (canonicalizePath, createDirectoryIfMissing, removePathForcibly)
+import System.FilePath ((</>))
+
+testDir :: String -> IO FilePath
+testDir prefix = canonicalizePath $ "test" </> prefix
+
+-- raises exception if cwd is not the package root so we don't damage anything
+tempDirIO :: String -> FilePath -> IO FilePath
+tempDirIO prefix path = do
+  base <- testDir prefix
+  let dir = base </> "temp"
+  removePathForcibly dir
+  createDirectoryIfMissing False dir
+  let absPath = dir </> path
+  createDirectoryIfMissing True absPath
+  return absPath
+
+tempDir :: String -> FilePath -> Neovim e FilePath
+tempDir prefix path =
+  liftIO $ tempDirIO prefix path
diff --git a/lib/Ribosome/Test/Functional.hs b/lib/Ribosome/Test/Functional.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Functional.hs
@@ -0,0 +1,60 @@
+module Ribosome.Test.Functional(
+  startPlugin,
+  fSpec,
+  functionalSpec,
+) where
+
+import Control.Monad (when)
+import Control.Monad.IO.Class
+import Control.Exception (finally)
+import Data.Foldable (traverse_)
+import System.Directory (getCurrentDirectory, createDirectoryIfMissing, removePathForcibly, doesFileExist)
+import System.FilePath (takeDirectory)
+import System.Console.ANSI (setSGR, SGR(SetColor, Reset), ConsoleLayer(Foreground), ColorIntensity(Dull), Color(Green))
+import Neovim (Neovim, vim_command')
+import Ribosome.Data.Ribo (Ribo)
+import Ribosome.Test.Exists (waitForPlugin)
+import Ribosome.Test.Embed (TestConfig(..), unsafeEmbeddedSpec, setupPluginEnv)
+
+jobstart :: MonadIO f => String -> f String
+jobstart cmd = do
+  dir <- liftIO getCurrentDirectory
+  return $ "call jobstart('" ++ cmd ++ "', { 'rpc': v:true, 'cwd': '" ++ dir ++ "' })"
+
+logFile :: TestConfig -> FilePath
+logFile conf = logPath conf ++ "-spec"
+
+startPlugin :: TestConfig -> Neovim env ()
+startPlugin conf = do
+  liftIO $ createDirectoryIfMissing True (takeDirectory (logPath conf))
+  liftIO $ removePathForcibly (logFile conf)
+  setupPluginEnv conf
+  cmd <- jobstart $ "stack run -- -l " ++ logFile conf ++ " -v INFO"
+  vim_command' cmd
+  waitForPlugin (pluginName conf) 0.1 3
+
+fSpec :: TestConfig -> Neovim env () -> Neovim env ()
+fSpec conf spec = startPlugin conf >> spec
+
+showLog' :: String -> IO ()
+showLog' output = do
+  putStrLn ""
+  setSGR [SetColor Foreground Dull Green]
+  putStrLn "plugin output:"
+  setSGR [Reset]
+  traverse_ putStrLn (lines output)
+  putStrLn ""
+
+showLog :: TestConfig -> IO ()
+showLog conf = do
+  let file = logFile conf
+  exists <- doesFileExist file
+  when exists $ do
+    output <- readFile file
+    case output of
+      [] -> return ()
+      o -> showLog' o
+
+functionalSpec :: TestConfig -> Ribo () () -> IO ()
+functionalSpec conf spec =
+  finally (unsafeEmbeddedSpec fSpec conf () spec) (showLog conf)
diff --git a/lib/Ribosome/Test/Unit.hs b/lib/Ribosome/Test/Unit.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Unit.hs
@@ -0,0 +1,32 @@
+module Ribosome.Test.Unit(
+  unitSpec,
+  tempDir,
+  tempFile,
+  uPrefix,
+) where
+
+import System.FilePath (takeDirectory, takeFileName, (</>))
+import Neovim (Neovim)
+import Ribosome.Data.Ribo (Ribo)
+import Ribosome.Test.Embed (TestConfig(..), setupPluginEnv, unsafeEmbeddedSpec)
+import qualified Ribosome.Test.File as F (tempDir)
+
+uPrefix :: String
+uPrefix = "u"
+
+uSpec :: TestConfig -> Neovim env () -> Neovim env ()
+uSpec conf spec = do
+  setupPluginEnv conf
+  spec
+
+unitSpec :: TestConfig -> e -> Ribo e () -> IO ()
+unitSpec =
+  unsafeEmbeddedSpec uSpec
+
+tempDir :: FilePath -> Neovim e FilePath
+tempDir = F.tempDir uPrefix
+
+tempFile :: FilePath -> Neovim e FilePath
+tempFile file = do
+  absDir <- tempDir $ takeDirectory file
+  return $ absDir </> takeFileName file
diff --git a/nvim.hs b/nvim.hs
deleted file mode 100644
--- a/nvim.hs
+++ /dev/null
@@ -1,6 +0,0 @@
-import Neovim
-
-import Proteome.Plugin (plugin)
-
-main :: IO ()
-main = neovim defaultConfig {plugins = [plugin]}
diff --git a/proteome.cabal b/proteome.cabal
--- a/proteome.cabal
+++ b/proteome.cabal
@@ -1,28 +1,205 @@
-name: proteome
-version: 0.1.0.0
-synopsis: neovim project manager
-description: neovim plugin for managing project-specific configuration and performing runtime tasks on projects
-license: MIT
-license-file: LICENSE
-author: Torsten Schmits
-maintainer: tek@tryp.io
-copyright: 2018 Torsten Schmits
-category: Neovim
-build-type: Simple
-extra-source-files: README.md
-cabal-version: >=1.10
-x-curation: uncurated-no-trustee-contact
+cabal-version: 1.12
 
-executable proteome
-  main-is: nvim.hs
+-- This file has been generated from package.yaml by hpack version 0.31.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: aeb4f9fc4dda14d1efb906445e7ff9e84fd39b5cf2bcf984c395b23c604c14fb
+
+name:           proteome
+version:        0.2.0.0
+synopsis:       neovim project manager
+description:    Please see the README on GitHub at <https://github.com/tek/proteome-hs>
+category:       Neovim
+homepage:       https://github.com/tek/proteome-hs#readme
+bug-reports:    https://github.com/tek/proteome-hs/issues
+author:         Torsten Schmits
+maintainer:     tek@tryp.io
+copyright:      2018 Torsten Schmits
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/tek/proteome-hs
+
+library
+  exposed-modules:
+      Proteome.Add
+      Proteome.Config
+      Proteome.Data.Env
+      Proteome.Data.Project
+      Proteome.Data.ProjectSpec
+      Proteome.Data.Proteome
+      Proteome.Init
+      Proteome.Log
+      Proteome.Plugin
+      Proteome.Project.Resolve
+      Proteome.Save
+      Proteome.Settings
+      Proteome.Tags
+      Proteome.Test.Config
+      Proteome.Test.Functional
+      Proteome.Test.Unit
+      Ribosome.Api.Option
+      Ribosome.Api.Response
+      Ribosome.Config.Settings
+      Ribosome.Data.Errors
+      Ribosome.Data.Maybe
+      Ribosome.Data.Ribo
+      Ribosome.Data.Ribosome
+      Ribosome.Internal.IO
+      Ribosome.Internal.NvimObject
+      Ribosome.Log
+      Ribosome.Test.Embed
+      Ribosome.Test.Exists
+      Ribosome.Test.File
+      Ribosome.Test.Functional
+      Ribosome.Test.Unit
+  other-modules:
+      Paths_proteome
   hs-source-dirs:
-    .,
-    lib
+      lib
+  build-depends:
+      MissingH
+    , ansi-terminal
+    , base >=4.7 && <5
+    , containers
+    , data-default-class
+    , deepseq
+    , directory
+    , filepath
+    , hslogger
+    , lens
+    , messagepack
+    , mtl
+    , nvim-hs
+    , pretty-terminal
+    , prettyprinter
+    , process
+    , resourcet
+    , split
+    , stm
+    , strings
+    , text
+    , time
+    , unliftio
+    , utf8-string
+  default-language: Haskell2010
+
+executable proteome-exe
+  main-is: Main.hs
   other-modules:
-    Proteome.Plugin,
-    Proteome.Init,
-    Proteome.Add
+      Paths_proteome
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
   build-depends:
-    base >= 4.7 && < 5,
-    nvim-hs >= 1 && < 2
+      MissingH
+    , ansi-terminal
+    , base >=4.7 && <5
+    , containers
+    , data-default-class
+    , deepseq
+    , directory
+    , filepath
+    , hslogger
+    , lens
+    , messagepack
+    , mtl
+    , nvim-hs
+    , pretty-terminal
+    , prettyprinter
+    , process
+    , proteome
+    , resourcet
+    , split
+    , stm
+    , strings
+    , text
+    , time
+    , unliftio
+    , utf8-string
+  default-language: Haskell2010
+
+test-suite proteome-functional
+  type: exitcode-stdio-1.0
+  main-is: SpecMain.hs
+  other-modules:
+      Config
+      ConfigSpec
+      InitSpec
+      Paths_proteome
+  hs-source-dirs:
+      test/f
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      HTF
+    , MissingH
+    , ansi-terminal
+    , base >=4.7 && <5
+    , containers
+    , data-default-class
+    , deepseq
+    , directory
+    , filepath
+    , hslogger
+    , lens
+    , messagepack
+    , mtl
+    , nvim-hs
+    , pretty-terminal
+    , prettyprinter
+    , process
+    , proteome
+    , resourcet
+    , split
+    , stm
+    , strings
+    , text
+    , time
+    , unliftio
+    , utf8-string
+  default-language: Haskell2010
+
+test-suite proteome-unit
+  type: exitcode-stdio-1.0
+  main-is: SpecMain.hs
+  other-modules:
+      Config
+      TagsSpec
+      Paths_proteome
+  hs-source-dirs:
+      test/u
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      HTF
+    , MissingH
+    , ansi-terminal
+    , base >=4.7 && <5
+    , containers
+    , data-default-class
+    , deepseq
+    , directory
+    , filepath
+    , hslogger
+    , lens
+    , messagepack
+    , mtl
+    , nvim-hs
+    , pretty-terminal
+    , prettyprinter
+    , process
+    , proteome
+    , resourcet
+    , split
+    , stm
+    , strings
+    , text
+    , time
+    , unliftio
+    , utf8-string
   default-language: Haskell2010
diff --git a/test/f/Config.hs b/test/f/Config.hs
new file mode 100644
--- /dev/null
+++ b/test/f/Config.hs
@@ -0,0 +1,15 @@
+module Config(
+  vars,
+) where
+
+import Neovim (toObject)
+import System.Directory (getCurrentDirectory)
+import Ribosome.Test.Embed (Vars(..))
+
+vars :: IO Vars
+vars = do
+  base <- getCurrentDirectory
+  return $ Vars [
+    ("proteome_project_base_dirs", toObject [base ++ "/test/f/fixtures/config/projects"]),
+    ("proteome_main_project_dir", toObject $ base ++ "/test/f/fixtures/config/projects/haskell/flagellum")
+    ]
diff --git a/test/f/ConfigSpec.hs b/test/f/ConfigSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/f/ConfigSpec.hs
@@ -0,0 +1,24 @@
+{-# OPTIONS_GHC -F -pgmF htfpp #-}
+
+module ConfigSpec(
+  htf_thisModulesTests
+) where
+
+import Control.Monad.IO.Class (liftIO)
+import Test.Framework
+import Data.MessagePack (Object(ObjectInt))
+import Neovim (vim_call_function, vim_get_var')
+import Ribosome.Data.Ribo (Ribo)
+import Proteome.Test.Functional (specWith)
+import Config (vars)
+
+configSpec :: Ribo env ()
+configSpec = do
+  _ <- vim_call_function "ProReadConfig" []
+  value <- vim_get_var' "flag"
+  liftIO $ assertEqual value (ObjectInt 1)
+
+test_config :: IO ()
+test_config = do
+  v <- vars
+  specWith v configSpec
diff --git a/test/f/InitSpec.hs b/test/f/InitSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/f/InitSpec.hs
@@ -0,0 +1,26 @@
+{-# OPTIONS_GHC -F -pgmF htfpp #-}
+
+module InitSpec(
+  htf_thisModulesTests
+) where
+
+import Control.Monad.IO.Class (liftIO)
+import Test.Framework
+import Ribosome.Data.Ribo (Ribo)
+import Ribosome.Config.Settings (setting)
+import qualified Proteome.Settings as S
+import Proteome.Data.Project (ProjectName(ProjectName), ProjectType(ProjectType))
+import Proteome.Test.Functional (specWith)
+import Config (vars)
+
+initSpec :: Ribo env ()
+initSpec = do
+  tpe <- setting S.mainType
+  name <- setting S.mainName
+  liftIO $ assertEqual name (ProjectName "flagellum")
+  liftIO $ assertEqual tpe (ProjectType "haskell")
+
+test_init :: IO ()
+test_init = do
+  v <- vars
+  specWith v initSpec
diff --git a/test/f/SpecMain.hs b/test/f/SpecMain.hs
new file mode 100644
--- /dev/null
+++ b/test/f/SpecMain.hs
@@ -0,0 +1,12 @@
+{-# OPTIONS_GHC -F -pgmF htfpp #-}
+
+module Main where
+
+import {-@ HTF_TESTS @-} ConfigSpec
+import {-@ HTF_TESTS @-} InitSpec
+import Test.Framework
+import Test.Framework.BlackBoxTest ()
+
+main :: IO ()
+main = do
+  htfMain htf_importedTests
diff --git a/test/u/Config.hs b/test/u/Config.hs
new file mode 100644
--- /dev/null
+++ b/test/u/Config.hs
@@ -0,0 +1,15 @@
+module Config(
+  vars,
+) where
+
+import Neovim (toObject)
+import System.Directory (getCurrentDirectory)
+import Ribosome.Test.Embed (Vars(..))
+
+vars :: IO Vars
+vars = do
+  base <- getCurrentDirectory
+  return $ Vars [
+    ("proteome_project_base_dirs", toObject [base ++ "/test/u/fixtures/projects"]),
+    ("proteome_main_project_dir", toObject $ base ++ "/test/u/fixtures/projects/haskell/flagellum")
+    ]
diff --git a/test/u/SpecMain.hs b/test/u/SpecMain.hs
new file mode 100644
--- /dev/null
+++ b/test/u/SpecMain.hs
@@ -0,0 +1,10 @@
+{-# OPTIONS_GHC -F -pgmF htfpp #-}
+
+module Main where
+
+import {-@ HTF_TESTS @-} TagsSpec
+import Test.Framework
+import Test.Framework.BlackBoxTest ()
+
+main :: IO ()
+main = htfMain htf_importedTests
diff --git a/test/u/TagsSpec.hs b/test/u/TagsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/u/TagsSpec.hs
@@ -0,0 +1,48 @@
+{-# OPTIONS_GHC -F -pgmF htfpp #-}
+
+module TagsSpec(
+  htf_thisModulesTests
+) where
+
+import Control.Monad.IO.Class (liftIO)
+import Control.Lens (set)
+import System.FilePath ((</>))
+import System.Directory (doesFileExist)
+import Test.Framework
+import Ribosome.Config.Settings (updateSetting)
+import qualified Ribosome.Data.Ribo as Ribo (modify)
+import Ribosome.Test.Unit (tempDir)
+import Proteome.Data.Proteome (Proteome)
+import Proteome.Data.Env (_mainProject)
+import Proteome.Data.Project (
+  ProjectName(..),
+  ProjectRoot(..),
+  ProjectType(..),
+  ProjectLang(..),
+  ProjectMetadata(DirProject),
+  _meta,
+  _lang,
+  )
+import qualified Proteome.Settings as S (tagsCommand, tagsArgs, tagsFork)
+import Proteome.Tags (proTags)
+import Proteome.Test.Unit (specWithDef)
+import Config (vars)
+
+main :: FilePath -> ProjectMetadata
+main root = DirProject (ProjectName "flagellum") (ProjectRoot root) (Just (ProjectType "haskell"))
+
+tagsSpec :: Proteome ()
+tagsSpec = do
+  root <- tempDir "projects/haskell/flagellum"
+  Ribo.modify $ set (_mainProject._meta) (main root)
+  Ribo.modify $ set (_mainProject._lang) (Just (ProjectLang "idris"))
+  updateSetting S.tagsCommand "touch"
+  updateSetting S.tagsArgs "tags-{langsComma}-{tagFile}"
+  updateSetting S.tagsFork False
+  proTags
+  let tagsFile = root </> "tags-idris-.tags"
+  exists <- liftIO $ doesFileExist tagsFile
+  liftIO $ assertBool exists
+
+test_tags :: IO ()
+test_tags = vars >>= specWithDef tagsSpec
