diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -13,6 +13,7 @@
 import SynapseCC.Logging
 import SynapseCC.Pipeline
 import SynapseCC.Types
+import SynapseCC.Wait (runWait)
 import SynapseCC.Watch (runWatch)
 
 -- ============================================================================
@@ -120,6 +121,13 @@
               if all (either (const False) (const True) . snd) results
                 then exitSuccess
                 else exitFailure
+
+    -- ------------------------------------------------------------------
+    -- synapse-cc wait [--timeout N] [--interval MS]
+    -- ------------------------------------------------------------------
+    CmdWait waitArgs -> do
+      ok <- runWait waitArgs
+      if ok then exitSuccess else exitFailure
 
     -- ------------------------------------------------------------------
     -- synapse-cc watch <backend> [plugins...] [--target name]
diff --git a/src/SynapseCC/CLI.hs b/src/SynapseCC/CLI.hs
--- a/src/SynapseCC/CLI.hs
+++ b/src/SynapseCC/CLI.hs
@@ -42,6 +42,7 @@
   <*> hostParser
   <*> portParser
   <*> optionsParser
+  <*> pure Nothing
 
 -- ============================================================================
 -- Command Parser (with subcommands: build, watch, init)
@@ -65,6 +66,8 @@
       (progDesc "Build client from backend schema (default)"))
  <> command "watch" (info watchCommandParser
       (progDesc "Watch backend for changes and incrementally rebuild"))
+ <> command "wait"  (info waitCommandParser
+      (progDesc "Block until all configured backends are reachable"))
  <> command "init"  (info initCommandParser
       (progDesc "Scaffold a synapse.config.json in the current directory"))
   )
@@ -77,7 +80,7 @@
   <*> portParser
   <*> optionsParser
   where
-    mkCmd (Just t) (Just b) host port opts = CmdBuild (Config t b host port opts)
+    mkCmd (Just t) (Just b) host port opts = CmdBuild (Config t b host port opts Nothing)
     mkCmd _        _        _    _    opts = CmdBuildFromConfig opts
 
 watchCommandParser :: Parser Command
@@ -103,6 +106,36 @@
        <> help "Poll interval in milliseconds (overrides synapse.config.json)"
         ))
   <*> watchOptionsParser
+
+waitCommandParser :: Parser Command
+waitCommandParser = fmap CmdWait $ WaitArgs
+  <$> optional (T.pack <$> argument str
+        ( metavar "BACKEND"
+       <> help "Backend to wait for (omit to wait for all from config)"
+        ))
+  <*> optional (T.pack <$> option str
+        ( long "url"
+       <> short 'u'
+       <> metavar "URL"
+       <> help "WebSocket URL (e.g. ws://127.0.0.1:4448). Requires BACKEND arg."
+        ))
+  <*> option auto
+        ( long "timeout"
+       <> short 't'
+       <> metavar "SECONDS"
+       <> value 30
+       <> showDefault
+       <> help "Max seconds to wait before giving up"
+        )
+  <*> option auto
+        ( long "interval"
+       <> short 'i'
+       <> metavar "MS"
+       <> value 500
+       <> showDefault
+       <> help "Poll interval in milliseconds"
+        )
+  <*> switch (long "debug" <> help "Enable debug logging")
 
 initCommandParser :: Parser Command
 initCommandParser = pure CmdInit
diff --git a/src/SynapseCC/Config.hs b/src/SynapseCC/Config.hs
--- a/src/SynapseCC/Config.hs
+++ b/src/SynapseCC/Config.hs
@@ -55,12 +55,16 @@
   requireNonEmpty "language" (scLanguage cfg)
   -- backend and url are required only when at least one target needs a backend.
   -- A target with generate: ["transport"] is backend-free.
-  let needsBackend = any ((/= ["transport"]) . tcGenerate) (Map.elems (scTargets cfg))
-  if needsBackend
-    then case scBackend cfg of
-           Nothing -> Left "\"backend\" is required when targets need a backend connection"
-           Just b  -> requireNonEmpty "backend" b
-    else Right ()
+  -- Each non-transport-only target needs a backend from either its own fields or top-level
+  let targetsNeedingBackend = filter ((/= ["transport"]) . tcGenerate . snd) (Map.toList (scTargets cfg))
+      missingBackend = filter (\(_, tc) ->
+        case (tcBackend tc, scBackend cfg) of
+          (Nothing, Nothing) -> True
+          _                  -> False
+        ) targetsNeedingBackend
+  case missingBackend of
+    ((name, _):_) -> Left $ "target \"" <> name <> "\": no \"backend\" specified (set per-target or top-level)"
+    []            -> Right ()
   if Map.null (scTargets cfg)
     then Left "At least one entry under \"targets\" is required"
     else mapM_ (validateTargetConfig (scTargets cfg)) (Map.toList (scTargets cfg))
@@ -71,13 +75,40 @@
         else Right ()
 
 validateTargetConfig :: Map Text TargetConfig -> (Text, TargetConfig) -> Either Text ()
-validateTargetConfig _all (name, tc) = do
+validateTargetConfig allTargets (name, tc) = do
   if null (tcGenerate tc)
     then Left $ "target \"" <> name <> "\": \"generate\" list must not be empty"
     else Right ()
   if null (tcOutputDir tc)
     then Left $ "target \"" <> name <> "\": \"outputDir\" must not be empty"
     else Right ()
+  -- Validate sharedTransport references
+  case tcSharedTransport tc of
+    Nothing -> Right ()
+    Just ref -> do
+      -- 1. Referenced target must exist
+      case Map.lookup ref allTargets of
+        Nothing -> Left $ "target \"" <> name <> "\": sharedTransport references unknown target \"" <> ref <> "\""
+        Just provider -> do
+          -- 2. Provider must have "transport" in its generate list
+          if "transport" `notElem` tcGenerate provider && tcGenerate provider /= ["all"]
+            then Left $ "target \"" <> name <> "\": sharedTransport target \"" <> ref
+                      <> "\" does not generate transport"
+            else Right ()
+          -- 3. Both targets must use the same transport mode
+          if tcTransport tc /= tcTransport provider
+            then Left $ "target \"" <> name <> "\": transport mode differs from sharedTransport target \"" <> ref <> "\""
+            else Right ()
+          -- 4. Consumer must NOT include "transport" or "rpc" in its generate list
+          if "transport" `elem` tcGenerate tc || "rpc" `elem` tcGenerate tc
+            then Left $ "target \"" <> name
+                      <> "\": generate must not include \"transport\" or \"rpc\" when using sharedTransport"
+            else Right ()
+          -- 5. No circular references
+          case tcSharedTransport provider of
+            Just _ -> Left $ "target \"" <> name <> "\": sharedTransport chain detected (\"" <> ref
+                           <> "\" also uses sharedTransport)"
+            Nothing -> Right ()
 
 -- ============================================================================
 -- Config-to-Runtime Conversions
@@ -101,9 +132,9 @@
 -- (--debug, --force, --no-install, etc.) are inherited as-is.
 buildConfigFromTarget :: Options -> SynapseConfig -> TargetConfig -> Config
 buildConfigFromTarget opts sc tc =
-  let url            = fromMaybe "ws://127.0.0.1:4444" (scUrl sc)
+  let url            = fromMaybe (fromMaybe "ws://127.0.0.1:4444" (scUrl sc)) (tcUrl tc)
       (host, port)   = parseWsUrl url
-      backendName    = fromMaybe "" (scBackend sc)
+      backendName    = fromMaybe (fromMaybe "" (scBackend sc)) (tcBackend tc)
   in Config
        { cfgTarget  = parseLanguage (scLanguage sc)
        , cfgBackend = Backend backendName
@@ -113,6 +144,7 @@
            { optOutput    = tcOutputDir tc
            , optTransport = tcTransport tc
            }
+       , cfgPlugins = tcPlugins tc
        }
 
 -- | Scaffold a synapse.config.json, using 'detectors' to infer the right
@@ -150,6 +182,6 @@
   { Pretty.confCompare = Pretty.keyOrder
       [ "schema", "language", "backend", "url", "packageManager"
       , "watch", "pollInterval", "hotReload"
-      , "targets", "generate", "transport", "outputDir", "smokePath"
+      , "targets", "generate", "transport", "outputDir", "smokePath", "sharedTransport"
       ]
   }
diff --git a/src/SynapseCC/Lock.hs b/src/SynapseCC/Lock.hs
--- a/src/SynapseCC/Lock.hs
+++ b/src/SynapseCC/Lock.hs
@@ -31,10 +31,11 @@
 
 -- | Per-target entry in synapse.lock, keyed by outputDir
 data LockTarget = LockTarget
-  { ltBackend   :: !Text
-  , ltIrHash    :: !Text
-  , ltTransport :: !Text
-  , ltFiles     :: !(Map Text Text)  -- ^ relPath → content hash
+  { ltBackend         :: !Text
+  , ltIrHash          :: !Text
+  , ltTransport       :: !Text
+  , ltFiles           :: !(Map Text Text)  -- ^ relPath → content hash
+  , ltSharedTransport :: !(Maybe Text)     -- ^ Name of provider target (if transport is shared)
   } deriving (Show, Eq)
 
 instance FromJSON LockTarget where
@@ -43,14 +44,17 @@
     <*> o Aeson..: "irHash"
     <*> o Aeson..: "transport"
     <*> o Aeson..: "files"
+    <*> o Aeson..:? "sharedTransport"
 
 instance ToJSON LockTarget where
-  toJSON lt = Aeson.object
+  toJSON lt = Aeson.object $
     [ "backend"   Aeson..= ltBackend lt
     , "irHash"    Aeson..= ltIrHash lt
     , "transport" Aeson..= ltTransport lt
     , "files"     Aeson..= ltFiles lt
-    ]
+    ] ++ case ltSharedTransport lt of
+           Nothing -> []
+           Just st -> ["sharedTransport" Aeson..= st]
 
 -- | Top-level synapse.lock structure
 data SynapseLock = SynapseLock
@@ -96,7 +100,7 @@
 lockPrettyConfig = Pretty.defConfig
   { Pretty.confCompare = Pretty.keyOrder
       [ "version", "synapseCC", "hubCodegen", "updatedAt", "targets"
-      , "backend", "irHash", "transport", "files"
+      , "backend", "irHash", "transport", "files", "sharedTransport"
       ]
   }
 
diff --git a/src/SynapseCC/Merge.hs b/src/SynapseCC/Merge.hs
--- a/src/SynapseCC/Merge.hs
+++ b/src/SynapseCC/Merge.hs
@@ -6,6 +6,7 @@
   ( computeFileHash
   , applyMerge
   , cleanRemovedFiles
+  , additiveMerge
   , MergeResult(..)
   ) where
 
@@ -14,6 +15,7 @@
 import qualified Data.ByteString.Base16 as Base16
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
 import Data.Text (Text)
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as TE
@@ -35,6 +37,7 @@
   | SafeToUpdate
   | Unchanged
   | UserModified
+  | SmartMerged
   deriving (Show, Eq)
 
 -- | Three-way merge decision table:
@@ -60,11 +63,13 @@
 
 -- | Result of applying a merge
 data MergeResult = MergeResult
-  { mrNew       :: [Text]  -- ^ New files written
-  , mrUpdated   :: [Text]  -- ^ Existing files safely updated
-  , mrUnchanged :: [Text]  -- ^ Files unchanged (write skipped)
-  , mrSkipped   :: [Text]  -- ^ Files skipped due to user modifications
-  , mrDeleted   :: [Text]  -- ^ Stale generated files removed
+  { mrNew          :: [Text]      -- ^ New files written
+  , mrUpdated      :: [Text]      -- ^ Existing files safely updated
+  , mrUnchanged    :: [Text]      -- ^ Files unchanged (write skipped)
+  , mrSkipped      :: [Text]      -- ^ Files skipped due to user modifications
+  , mrDeleted      :: [Text]      -- ^ Stale generated files removed
+  , mrMerged       :: [Text]      -- ^ Files smart-merged (new content added, user mods preserved)
+  , mrMergedHashes :: Map Text Text  -- ^ relPath -> merged hash for smart-merged files
   } deriving (Show, Eq)
 
 -- | Apply three-way merge: write generated files to disk, skipping user-modified ones.
@@ -78,7 +83,7 @@
   results <- mapM processFile (Map.toList generatedFiles)
   pure $ foldr addResult emptyResult results
   where
-    emptyResult = MergeResult [] [] [] [] []
+    emptyResult = MergeResult [] [] [] [] [] [] Map.empty
 
     processFile (relPath, content) = do
       let fullPath   = outputDir </> T.unpack relPath
@@ -87,10 +92,19 @@
       currentHash <- readCurrentHash fullPath
       let status = determineStatus cachedHash currentHash newHash
       case status of
-        NewFile      -> writeFile' fullPath content >> pure (relPath, NewFile)
-        SafeToUpdate -> writeFile' fullPath content >> pure (relPath, SafeToUpdate)
-        Unchanged    -> pure (relPath, Unchanged)
-        UserModified -> pure (relPath, UserModified)
+        NewFile      -> writeFile' fullPath content >> pure (relPath, NewFile, Nothing)
+        SafeToUpdate -> writeFile' fullPath content >> pure (relPath, SafeToUpdate, Nothing)
+        Unchanged    -> pure (relPath, Unchanged, Nothing)
+        SmartMerged  -> pure (relPath, SmartMerged, Nothing)  -- not reached from determineStatus
+        UserModified -> do
+          currentBytes <- BS.readFile fullPath
+          let currentContent = TE.decodeUtf8With TEE.lenientDecode currentBytes
+          case additiveMerge content currentContent of
+            Just merged -> do
+              writeFile' fullPath merged
+              let mergedHash = computeFileHash merged
+              pure (relPath, SmartMerged, Just mergedHash)
+            Nothing -> pure (relPath, UserModified, Nothing)
 
     readCurrentHash path = do
       exists <- doesFileExist path
@@ -104,10 +118,77 @@
       createDirectoryIfMissing True (takeDirectory path)
       BS.writeFile path (TE.encodeUtf8 content)
 
-    addResult (p, NewFile)      mr = mr { mrNew       = p : mrNew       mr }
-    addResult (p, SafeToUpdate) mr = mr { mrUpdated   = p : mrUpdated   mr }
-    addResult (p, Unchanged)    mr = mr { mrUnchanged = p : mrUnchanged mr }
-    addResult (p, UserModified) mr = mr { mrSkipped   = p : mrSkipped   mr }
+    addResult (p, NewFile, _)      mr = mr { mrNew       = p : mrNew       mr }
+    addResult (p, SafeToUpdate, _) mr = mr { mrUpdated   = p : mrUpdated   mr }
+    addResult (p, Unchanged, _)    mr = mr { mrUnchanged = p : mrUnchanged mr }
+    addResult (p, UserModified, _) mr = mr { mrSkipped   = p : mrSkipped   mr }
+    addResult (p, SmartMerged, mh) mr = mr
+      { mrMerged       = p : mrMerged mr
+      , mrMergedHashes = case mh of
+          Just h  -> Map.insert p h (mrMergedHashes mr)
+          Nothing -> mrMergedHashes mr
+      }
+
+-- | Additive merge: insert novel lines from new content into user-modified current content.
+-- Returns Nothing if files are too divergent (<50% shared lines) or an anchor can't be found.
+additiveMerge :: Text -> Text -> Maybe Text
+additiveMerge newContent currentContent
+  | newLines == currentLines = Just currentContent  -- identical, no-op
+  | sharedRatio < 0.5       = Nothing               -- too divergent
+  | null insertionBlocks    = Just currentContent    -- no novel lines
+  | otherwise               = applyInsertions currentLines insertionBlocks
+  where
+    newLines     = T.lines newContent
+    currentLines = T.lines currentContent
+    currentSet   = Set.fromList currentLines
+
+    -- Count how many new lines exist in current
+    sharedCount  = length $ filter (`Set.member` currentSet) newLines
+    sharedRatio  = fromIntegral sharedCount / max 1 (fromIntegral (length newLines)) :: Double
+
+    -- Walk newLines, grouping consecutive novel lines with their anchor
+    -- An anchor is the last shared line before a block of novel lines
+    insertionBlocks = extractBlocks Nothing [] [] newLines
+
+    -- Extract insertion blocks: (Maybe anchor, [novel lines])
+    extractBlocks :: Maybe Text -> [(Maybe Text, [Text])] -> [Text] -> [Text] -> [(Maybe Text, [Text])]
+    extractBlocks _anchor acc novelAcc [] =
+      if null novelAcc then reverse acc
+      else reverse ((Nothing, reverse novelAcc) : acc)  -- trailing novel lines get no anchor
+    extractBlocks anchor acc novelAcc (l:ls)
+      | l `Set.member` currentSet =
+          let acc' = if null novelAcc then acc
+                     else (anchor, reverse novelAcc) : acc
+          in extractBlocks (Just l) acc' [] ls
+      | otherwise =
+          extractBlocks anchor acc (l : novelAcc) ls
+
+    -- Apply insertion blocks into currentLines
+    applyInsertions :: [Text] -> [(Maybe Text, [Text])] -> Maybe Text
+    applyInsertions curr blocks = go curr blocks 0
+      where
+        go :: [Text] -> [(Maybe Text, [Text])] -> Int -> Maybe Text
+        go remaining [] _ = Just $ T.unlines (remaining)
+        go remaining ((Nothing, novel):rest) pos =
+          -- No anchor: prepend to result (novel lines before any shared line)
+          go (novel ++ remaining) rest pos
+        go remaining ((Just anchor, novel):rest) pos =
+          -- Find anchor in remaining lines (forward scan)
+          case findAnchor anchor remaining of
+            Nothing -> Nothing  -- can't find anchor, bail
+            Just idx ->
+              let (before, afterAnchor) = splitAt (idx + 1) remaining
+                  remaining' = before ++ novel ++ afterAnchor
+                  newPos = pos + idx + 1 + length novel
+              in go remaining' rest newPos
+
+    -- Find first occurrence of anchor line in a list
+    findAnchor :: Text -> [Text] -> Maybe Int
+    findAnchor _anchor [] = Nothing
+    findAnchor anchor ls  = go' 0 ls
+      where
+        go' _ []     = Nothing
+        go' i (x:xs) = if x == anchor then Just i else go' (i + 1) xs
 
 -- | Delete stale generated files: those present in the previous generation's
 -- cached hashes but absent from the new generation.
diff --git a/src/SynapseCC/Pipeline.hs b/src/SynapseCC/Pipeline.hs
--- a/src/SynapseCC/Pipeline.hs
+++ b/src/SynapseCC/Pipeline.hs
@@ -8,7 +8,7 @@
   ) where
 
 import Control.Exception (try, SomeException)
-import Control.Monad (when, unless, forM_)
+import Control.Monad (when, unless, forM_, forM)
 import Data.Aeson (FromJSON, eitherDecodeStrict, eitherDecodeFileStrict)
 import qualified Data.Aeson as Aeson
 import qualified Data.ByteString as BS
@@ -24,10 +24,12 @@
 import GHC.Generics (Generic)
 import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getCurrentDirectory, listDirectory)
 import System.Exit (ExitCode(..))
-import System.FilePath ((</>))
+import System.FilePath ((</>), splitPath)
 
 import Synapse.Monad (initEnv, runSynapseM, SynapseError(..))
 import Synapse.IR.Builder (buildIR)
+import qualified Synapse.Log as Log
+import qualified Katip
 
 import SynapseCC.Benchmark (timeStep, baselinePath, reportBenchmarks)
 import SynapseCC.Config (buildConfigFromTarget)
@@ -148,6 +150,29 @@
     , "}"
     ]
 
+-- | Generate tsconfig.json for integration mode (generated code inside a host project).
+-- Enables project references (composite, declaration, declarationMap) so the host
+-- project can reference the generated directory as a sub-project with its own settings.
+generateIntegrationTsconfig :: Text
+generateIntegrationTsconfig = T.unlines
+    [ "{"
+    , "  \"compilerOptions\": {"
+    , "    \"target\": \"ES2022\","
+    , "    \"module\": \"ES2022\","
+    , "    \"moduleResolution\": \"bundler\","
+    , "    \"composite\": true,"
+    , "    \"declaration\": true,"
+    , "    \"declarationMap\": true,"
+    , "    \"strict\": true,"
+    , "    \"skipLibCheck\": true,"
+    , "    \"esModuleInterop\": true,"
+    , "    \"outDir\": \"../dist/generated\","
+    , "    \"rootDir\": \".\""
+    , "  },"
+    , "  \"include\": [\"**/*.ts\"]"
+    , "}"
+    ]
+
 -- | Run the full pipeline without cache
 runFullPipeline :: Config -> ToolLocations -> IO (Either SynapseCCError CompiledPath)
 runFullPipeline config tools = do
@@ -243,6 +268,14 @@
           irContent <- BS.readFile (unIRPath irPath)
           BS.writeFile (outputDir </> "ir.json") irContent
 
+          -- Log smart-merged files (new content added, user modifications preserved)
+          let merged = Merge.mrMerged mergeResult
+          unless (null merged) $ do
+            logInfo $ "  ✓ " <> T.pack (show (length merged))
+                    <> " file(s) smart-merged (new content added, user modifications preserved)"
+            when debug $ forM_ merged $ \f ->
+              logDebug debug $ "      - " <> f
+
           -- Log skipped files (user modifications preserved)
           let skipped = Merge.mrSkipped mergeResult
           unless (null skipped) $ do
@@ -253,12 +286,14 @@
 
           let genPath = GeneratedPath outputDir
 
-          -- Write synapse-cc's tsconfig to the output dir (standalone mode only).
-          -- Integration mode: host project owns tsconfig at its root; we don't touch it.
-          -- The tsconfig only covers *.ts (not test/) so tsc never sees bun:test imports.
-          unless isIntegration $ do
-            logDebug debug "  Writing tsconfig.json"
-            TIO.writeFile (outputDir </> "tsconfig.json") (generateTsconfig (optTransport opts))
+          -- Write synapse-cc's tsconfig to the output dir.
+          -- Standalone mode: tight config (noEmit, transport-specific types).
+          -- Integration mode: project-references config (composite, declaration).
+          do logDebug debug "  Writing tsconfig.json"
+             let tsconfig = if isIntegration
+                              then generateIntegrationTsconfig
+                              else generateTsconfig (optTransport opts)
+             TIO.writeFile (outputDir </> "tsconfig.json") tsconfig
 
           -- Step 3: Install dependencies (if enabled)
           -- Standalone: deps go into the generated dir (genPath) — it owns its own package.json.
@@ -333,7 +368,11 @@
                   case testResult of
                     Left err -> pure $ Left err
                     Right () -> do
-                      writeCache config tools irPath compiledPath out
+                      -- Patch file hashes: replace hashes for smart-merged files
+                      -- with their merged hashes so subsequent runs see them as SafeToUpdate
+                      let patchedHashes = Map.union (Merge.mrMergedHashes mergeResult) (coFileHashes out)
+                          patchedOut = out { coFileHashes = patchedHashes }
+                      writeCache config tools irPath compiledPath patchedOut
 
                       -- Benchmarks
                       pipelineEnd <- getCurrentTime
@@ -394,10 +433,11 @@
                     BrowserTransport -> "browser"
       Backend bkName = cfgBackend config
       lt = Lock.LockTarget
-             { Lock.ltBackend   = bkName
-             , Lock.ltIrHash    = irHash
-             , Lock.ltTransport = transport
-             , Lock.ltFiles     = coFileHashes codegenOutput
+             { Lock.ltBackend         = bkName
+             , Lock.ltIrHash          = irHash
+             , Lock.ltTransport       = transport
+             , Lock.ltFiles           = coFileHashes codegenOutput
+             , Lock.ltSharedTransport = Nothing
              }
   lock <- fromMaybe Lock.emptySynapseLock <$> Lock.readSynapseLock
   now  <- getCurrentTime
@@ -471,14 +511,63 @@
 -- | Run a build pass for every target in a SynapseConfig.
 -- CLI options override per-target transport/outputDir only when explicitly
 -- different from their defaults; targets own those fields.
+-- Providers (no sharedTransport) are built first, then consumers (with sharedTransport)
+-- get shim files that re-export from the provider's output.
 -- Returns one result per target (name, outcome).
 runBuildFromConfig
   :: SynapseConfig
   -> Options       -- ^ CLI flags (--debug, --force, --no-install, etc.)
   -> ToolLocations
   -> IO [(T.Text, Either SynapseCCError CompiledPath)]
-runBuildFromConfig sc opts tools =
-  mapM buildTarget (Map.toList (scTargets sc))
+runBuildFromConfig sc opts tools = do
+  let allTargets  = Map.toList (scTargets sc)
+      -- Partition: providers first (no sharedTransport), then consumers
+      (providers, consumers) = foldr partitionTarget ([], []) allTargets
+      partitionTarget (name, tc) (ps, cs) =
+        case tcSharedTransport tc of
+          Nothing -> ((name, tc) : ps, cs)
+          Just _  -> (ps, (name, tc) : cs)
+
+  -- Build providers first
+  providerResults <- mapM buildTarget providers
+
+  -- Build consumers: run hub-codegen for plugins only, then write shim files
+  consumerResults <- forM consumers $ \(name, tc) -> do
+    let config = buildConfigFromTarget opts sc tc
+    logInfo $ "Building \"" <> name <> "\" → " <> T.pack (optOutput (cfgOptions config))
+    logDebug (optDebug opts) $ "  target \"" <> name <> "\" uses sharedTransport"
+    -- Run normal pipeline for plugins (generate list should be ["plugins"])
+    result <- runPipeline config tools
+    -- Write re-export shim files and update lock
+    case (result, tcSharedTransport tc) of
+      (Right _, Just providerName) ->
+        case Map.lookup providerName (scTargets sc) of
+          Just providerTc -> do
+            let shimFiles = generateShimFiles (tcOutputDir tc) (tcOutputDir providerTc)
+            createDirectoryIfMissing True (tcOutputDir tc)
+            forM_ (Map.toList shimFiles) $ \(fileName, content) -> do
+              let filePath = tcOutputDir tc </> T.unpack fileName
+              TIO.writeFile filePath content
+              logDebug (optDebug opts) $ "  Wrote shim: " <> T.pack filePath
+            -- Update lock file with shim file hashes
+            lock <- fromMaybe Lock.emptySynapseLock <$> Lock.readSynapseLock
+            let outputKey = T.pack (tcOutputDir tc)
+            case Map.lookup outputKey (Lock.slTargets lock) of
+              Just lt -> do
+                let shimHashes = Map.mapWithKey (\_ content -> Merge.computeFileHash content) shimFiles
+                    updatedLt = lt
+                      { Lock.ltSharedTransport = Just providerName
+                      , Lock.ltFiles = Map.union shimHashes (Lock.ltFiles lt)
+                      }
+                Lock.writeSynapseLock $ lock
+                  { Lock.slTargets = Map.insert outputKey updatedLt (Lock.slTargets lock) }
+              Nothing -> pure ()
+            logSuccess $ "Shim files written for \"" <> name <> "\""
+          Nothing -> pure ()  -- Should not happen (validated in Config.hs)
+      _ -> pure ()
+    pure (name, result)
+
+  pure $ providerResults ++ consumerResults
   where
     buildTarget (name, tc) = do
       let config = buildConfigFromTarget opts sc tc
@@ -489,6 +578,40 @@
                    else runPipeline config tools
       pure (name, result)
 
+-- | Generate re-export shim files for a child target that shares transport
+-- with a parent target. Computes the relative path from child → parent
+-- and returns a map of filename → shim content.
+generateShimFiles :: FilePath -> FilePath -> Map.Map Text Text
+generateShimFiles childDir parentDir =
+  let relPath = computeRelativePath childDir parentDir
+      shimContent file = "export * from '" <> relPath <> "/" <> file <> "';\n"
+  in Map.fromList
+    [ ("transport.ts", shimContent "transport")
+    , ("rpc.ts",       shimContent "rpc")
+    , ("types.ts",     shimContent "types")
+    ]
+
+-- | Compute the relative path from childDir to parentDir.
+-- E.g., "generated/substrate" → "generated" yields ".."
+-- E.g., "src/gen/sub" → "src/gen" yields ".."
+-- E.g., "generated-sub" → "generated" yields "../generated"
+computeRelativePath :: FilePath -> FilePath -> Text
+computeRelativePath childDir parentDir =
+  let -- splitPath keeps trailing '/' on components; strip for comparison
+      normalize = map (filter (/= '/'))
+      childParts  = normalize (splitPath childDir)
+      parentParts = normalize (splitPath parentDir)
+      -- Find common prefix length
+      commonLen   = length $ takeWhile id $ zipWith (==) childParts parentParts
+      -- Steps up from child to common ancestor
+      stepsUp     = length childParts - commonLen
+      -- Steps down from common ancestor to parent
+      stepsDown   = drop commonLen parentParts
+      upParts     = replicate stepsUp ".."
+      relParts    = upParts ++ stepsDown
+      rel = T.intercalate "/" $ map T.pack relParts
+  in if T.null rel then "." else rel
+
 -- ============================================================================
 -- Transport-Only Pipeline (no backend connection required)
 -- ============================================================================
@@ -573,7 +696,8 @@
   -- Call plexus-synapse library directly (no subprocess).
   -- Pass [] as path: the backend is encoded in the env; [] = walk from root.
   logDebug debug $ "  Connecting to " <> host <> ":" <> T.pack (show port)
-  env <- initEnv host port bkName
+  logger <- Log.makeLogger Katip.ErrorS
+  env <- initEnv host port bkName logger
   -- Wrap in try to catch hard exceptions from child-plugin fetch errors
   rawResult <- try (runSynapseM env (buildIR generatorInfo []))
   result <- case rawResult of
@@ -656,9 +780,14 @@
       target         = cfgTarget config
       targetArg      = T.unpack (targetToText target)
       backendUrl     = "ws://" <> T.unpack (cfgHost config) <> ":" <> T.unpack (cfgPort config)
+      -- nsFilter: incremental mode (--generate plugins --plugins ...)
       filterArgs = case nsFilter of
         Nothing  -> []
         Just nss -> ["--generate", "plugins", "--plugins", T.unpack (T.intercalate "," nss)]
+      -- cfgPlugins: full generation with plugin filtering (--plugins ... only, no --generate plugins)
+      pluginFilterArgs = case cfgPlugins config of
+        Just ps | null filterArgs -> ["--plugins", T.unpack (T.intercalate "," ps)]
+        _                         -> []
       args =
         [ "--target",           targetArg
         , "--output-format",    "json"
@@ -668,6 +797,7 @@
         , "--backend-url",      backendUrl
         ]
         ++ filterArgs
+        ++ pluginFilterArgs
         ++ [ unIRPath irPath ]
 
   result <- runProcess hubCodegenPath args Nothing debug
diff --git a/src/SynapseCC/Types.hs b/src/SynapseCC/Types.hs
--- a/src/SynapseCC/Types.hs
+++ b/src/SynapseCC/Types.hs
@@ -26,6 +26,7 @@
     -- * Commands (CLI subcommands)
   , Command(..)
   , WatchArgs(..)
+  , WaitArgs(..)
 
     -- * Tool Locations
   , ToolLocations(..)
@@ -87,6 +88,7 @@
   , cfgHost           :: !Text
   , cfgPort           :: !Text
   , cfgOptions        :: !Options
+  , cfgPlugins        :: !(Maybe [Text])  -- ^ Plugin filter (Nothing = all)
   } deriving stock (Show, Eq, Generic)
 
 -- | Target language for code generation
@@ -179,10 +181,14 @@
 
 -- | Per-target configuration entry in synapse.config.json
 data TargetConfig = TargetConfig
-  { tcGenerate   :: ![Text]           -- ^ Selectors: ["transport","rpc","plugins"] or ["all"]
-  , tcTransport  :: !TransportType    -- ^ Transport for this target
-  , tcOutputDir  :: !FilePath         -- ^ Where to write generated files
-  , tcSmokePath  :: !(Maybe Text)     -- ^ Relative path to transport for smoke targets
+  { tcGenerate        :: ![Text]           -- ^ Selectors: ["transport","rpc","plugins"] or ["all"]
+  , tcTransport       :: !TransportType    -- ^ Transport for this target
+  , tcOutputDir       :: !FilePath         -- ^ Where to write generated files
+  , tcSmokePath       :: !(Maybe Text)     -- ^ Relative path to transport for smoke targets
+  , tcBackend         :: !(Maybe Text)     -- ^ Per-target backend override
+  , tcUrl             :: !(Maybe Text)     -- ^ Per-target WebSocket URL override
+  , tcPlugins         :: !(Maybe [Text])   -- ^ Plugin filter (Nothing = all)
+  , tcSharedTransport :: !(Maybe Text)     -- ^ Name of another target whose transport to reuse
   } deriving stock (Show, Eq, Generic)
 
 instance FromJSON TargetConfig where
@@ -191,6 +197,10 @@
     <*> (parseTransport =<< o Aeson..: "transport")
     <*> o Aeson..: "outputDir"
     <*> o Aeson..:? "smokePath"
+    <*> o Aeson..:? "backend"
+    <*> o Aeson..:? "url"
+    <*> o Aeson..:? "plugins"
+    <*> o Aeson..:? "sharedTransport"
     where
       parseTransport :: Text -> Parser TransportType
       parseTransport "ws"      = pure WsTransport
@@ -205,6 +215,18 @@
     ] ++ case tcSmokePath tc of
            Nothing -> []
            Just p  -> ["smokePath" Aeson..= p]
+      ++ case tcBackend tc of
+           Nothing -> []
+           Just b  -> ["backend" Aeson..= b]
+      ++ case tcUrl tc of
+           Nothing -> []
+           Just u  -> ["url" Aeson..= u]
+      ++ case tcPlugins tc of
+           Nothing -> []
+           Just ps -> ["plugins" Aeson..= ps]
+      ++ case tcSharedTransport tc of
+           Nothing -> []
+           Just st -> ["sharedTransport" Aeson..= st]
 
 -- | Top-level synapse.config.json configuration.
 -- \"backend\" and \"url\" are optional when every target has generate: [\"transport\"]
@@ -251,10 +273,14 @@
   , scTargets        = Map.fromList
       [ ( "client"
         , TargetConfig
-            { tcGenerate  = ["transport", "rpc", "plugins"]
-            , tcTransport = WsTransport
-            , tcOutputDir = "src/lib/plexus"
-            , tcSmokePath = Nothing
+            { tcGenerate        = ["transport", "rpc", "plugins"]
+            , tcTransport       = WsTransport
+            , tcOutputDir       = "src/lib/plexus"
+            , tcSmokePath       = Nothing
+            , tcBackend         = Nothing
+            , tcUrl             = Nothing
+            , tcPlugins         = Nothing
+            , tcSharedTransport = Nothing
             }
         )
       ]
@@ -270,6 +296,7 @@
   | CmdBuildFromConfig      -- ^ Build all targets from synapse.config.json
       Options               -- ^ CLI flags that override per-target config
   | CmdWatch WatchArgs      -- ^ Watch mode
+  | CmdWait WaitArgs        -- ^ Wait for backends to become reachable
   | CmdInit                 -- ^ Scaffold synapse.config.json
   deriving stock (Show, Eq)
 
@@ -280,6 +307,15 @@
   , waTarget    :: !(Maybe Text) -- ^ Pin to a single named config target
   , waInterval  :: !(Maybe Int)  -- ^ Poll interval override in ms (overrides config)
   , waOptions   :: !Options      -- ^ Inherited options (host, port, cache, debug, etc.)
+  } deriving stock (Show, Eq)
+
+-- | Arguments for the wait subcommand
+data WaitArgs = WaitArgs
+  { waitBackend  :: !(Maybe Text)  -- ^ Specific backend (if None, use all from config)
+  , waitUrl      :: !(Maybe Text)  -- ^ Explicit URL (requires backend)
+  , waitTimeout  :: !Int           -- ^ Max seconds to wait (default 30)
+  , waitInterval :: !Int           -- ^ Poll interval in ms (default 500)
+  , waitDebug    :: !Bool          -- ^ Debug logging
   } deriving stock (Show, Eq)
 
 -- ============================================================================
diff --git a/src/SynapseCC/Wait.hs b/src/SynapseCC/Wait.hs
new file mode 100644
--- /dev/null
+++ b/src/SynapseCC/Wait.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Wait command: block until configured backends are reachable.
+--
+-- Usage:
+--   synapse-cc wait                         -- wait for all backends in config
+--   synapse-cc wait monochrome              -- wait for one backend (URL from config)
+--   synapse-cc wait monochrome -u ws://...: -- wait for explicit backend+URL
+module SynapseCC.Wait
+  ( runWait
+  ) where
+
+import Control.Concurrent (threadDelay)
+import Control.Exception (try, SomeException)
+import qualified Data.Map.Strict as Map
+import Data.Maybe (fromMaybe)
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time.Clock (getCurrentTime, diffUTCTime)
+import Control.Monad (when)
+
+import qualified Data.Aeson as Aeson
+import Plexus.Client (SubstrateConfig(..), defaultConfig)
+import qualified Plexus.Transport as Transport
+
+import SynapseCC.Config (loadSynapseConfig, parseWsUrl)
+import SynapseCC.Logging
+import SynapseCC.Types
+
+-- | Extract all unique (backend, url) pairs from the config.
+collectEndpoints :: SynapseConfig -> [(Text, Text)]
+collectEndpoints sc =
+  let defaultUrl = fromMaybe "ws://127.0.0.1:4444" (scUrl sc)
+      defaultBk  = fromMaybe "substrate" (scBackend sc)
+      pairs = [ let bk  = fromMaybe defaultBk (tcBackend tc)
+                    url = fromMaybe defaultUrl (tcUrl tc)
+                in (bk, url)
+              | tc <- Map.elems (scTargets sc)
+              ]
+  in Map.toList (Map.fromList pairs)
+
+-- | Try to connect to a backend by calling {backend}.hash
+probeBackend :: Text -> Text -> IO (Either Text ())
+probeBackend backend url = do
+  let (host, portTxt) = parseWsUrl url
+      port = read (T.unpack portTxt) :: Int
+      cfg = (defaultConfig backend)
+              { substrateHost = T.unpack host
+              , substratePort = port
+              }
+      method = backend <> ".hash"
+  outcome <- try $ Transport.rpcCallWith cfg method Aeson.Null
+  case outcome of
+    Left (ex :: SomeException) -> pure $ Left $ T.pack (show ex)
+    Right (Left err)           -> pure $ Left $ T.pack (show err)
+    Right (Right _)            -> pure $ Right ()
+
+-- | Determine which endpoints to wait for based on args.
+resolveEndpoints :: WaitArgs -> IO (Either Text [(Text, Text)])
+resolveEndpoints args = case waitBackend args of
+  -- Explicit backend + URL: no config needed
+  Just bk | Just url <- waitUrl args ->
+    pure $ Right [(bk, url)]
+
+  -- Explicit backend, URL from config
+  Just bk -> do
+    cfgResult <- loadSynapseConfig
+    case cfgResult of
+      Left err -> pure $ Left err
+      Right sc -> do
+        let allEps = collectEndpoints sc
+        case lookup bk allEps of
+          Just url -> pure $ Right [(bk, url)]
+          Nothing  ->
+            -- Backend not in config — try top-level URL as fallback
+            let url = fromMaybe "ws://127.0.0.1:4444" (scUrl sc)
+            in pure $ Right [(bk, url)]
+
+  -- No backend specified: all from config
+  Nothing -> do
+    cfgResult <- loadSynapseConfig
+    case cfgResult of
+      Left err -> pure $ Left err
+      Right sc -> do
+        let eps = collectEndpoints sc
+        if null eps
+          then pure $ Left "no backends configured in synapse.config.json"
+          else pure $ Right eps
+
+-- | Run the wait command. Returns True on success, False on timeout/error.
+runWait :: WaitArgs -> IO Bool
+runWait args = do
+  epResult <- resolveEndpoints args
+  case epResult of
+    Left err -> do
+      logError err
+      pure False
+
+    Right endpoints -> do
+      let n = length endpoints
+          names = T.intercalate ", " [bk <> " (" <> url <> ")" | (bk, url) <- endpoints]
+      logStep $ "Waiting for " <> T.pack (show n)
+             <> " backend" <> (if n > 1 then "s" else "")
+             <> ": " <> names
+             <> " (timeout: " <> T.pack (show (waitTimeout args)) <> "s)"
+
+      startTime <- getCurrentTime
+      let timeoutSecs = fromIntegral (waitTimeout args) :: Double
+          intervalUs  = waitInterval args * 1000
+
+          loop :: Set Text -> IO Bool
+          loop remaining
+            | Set.null remaining = pure True
+            | otherwise = do
+                now <- getCurrentTime
+                let elapsed = realToFrac (diffUTCTime now startTime) :: Double
+                if elapsed >= timeoutSecs then do
+                  logError $ "Timed out after " <> T.pack (show (waitTimeout args))
+                          <> "s waiting for: "
+                          <> T.intercalate ", " (Set.toList remaining)
+                  pure False
+                else do
+                  newRemaining <- probeAll remaining endpoints
+                  if Set.null newRemaining
+                    then pure True
+                    else do
+                      threadDelay intervalUs
+                      loop newRemaining
+
+          probeAll :: Set Text -> [(Text, Text)] -> IO (Set Text)
+          probeAll remaining eps = go remaining eps
+            where
+              go acc [] = pure acc
+              go acc ((bk, url):rest)
+                | not (Set.member bk acc) = go acc rest
+                | otherwise = do
+                    result <- probeBackend bk url
+                    case result of
+                      Right () -> do
+                        logSuccess $ bk <> " reachable"
+                        go (Set.delete bk acc) rest
+                      Left _ -> do
+                        when (waitDebug args) $
+                          logInfo $ "  " <> bk <> " not yet reachable"
+                        go acc rest
+
+      let backendNames = Set.fromList [bk | (bk, _) <- endpoints]
+      loop backendNames
diff --git a/src/SynapseCC/Watch.hs b/src/SynapseCC/Watch.hs
--- a/src/SynapseCC/Watch.hs
+++ b/src/SynapseCC/Watch.hs
@@ -42,6 +42,8 @@
 import Plexus.Types (PlexusStreamItem(..))
 import Synapse.Monad (initEnv, runSynapseM)
 import Synapse.IR.Builder (buildIR)
+import qualified Synapse.Log as Log
+import qualified Katip
 
 import SynapseCC.Cache (expandTilde)
 import qualified SynapseCC.Config as Config
@@ -206,7 +208,8 @@
   t0 <- getCurrentTime
 
   -- Fetch fresh IR via plexus-synapse library
-  env <- initEnv host port bkName
+  logger <- Log.makeLogger Katip.ErrorS
+  env <- initEnv host port bkName logger
   irResult <- runSynapseM env (buildIR generatorInfo [])
 
   case irResult of
diff --git a/synapse-cc.cabal b/synapse-cc.cabal
--- a/synapse-cc.cabal
+++ b/synapse-cc.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               synapse-cc
-version:            0.2.0
+version:            0.3.2
 synopsis:           Unified compiler toolchain for Plexus backends
 description:
   synapse-cc orchestrates the complete pipeline from backend schema
@@ -31,6 +31,7 @@
                     , SynapseCC.Language
                     , SynapseCC.Logging
                     , SynapseCC.Benchmark
+                    , SynapseCC.Wait
                     , SynapseCC.Watch
     build-depends:    base >= 4.14 && < 5
                     , optparse-applicative >= 0.18
@@ -54,6 +55,7 @@
                     , fsnotify >= 0.4
                     , plexus-protocol
                     , plexus-synapse
+                    , katip >= 0.8 && < 0.9
     autogen-modules:  Paths_synapse_cc
     other-modules:    Paths_synapse_cc
     hs-source-dirs:   src
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -48,6 +48,7 @@
   , detectTauri, detectVite, detectNextJS, detectNodeProject
   )
 import SynapseCC.Discover (discoverTools)
+import SynapseCC.Merge (additiveMerge)
 import SynapseCC.Pipeline (runPipeline)
 import SynapseCC.Types
   ( Config(..), Backend(..), Target(..), Options(..), defaultOptions, formatError
@@ -696,7 +697,72 @@
         after <- TIO.readFile target
         after `shouldNotBe` userEdit
 
+      it "user edit survives rerun AND new methods appear via smart merge" $ do
+        -- If the backend has new methods, additiveMerge should insert them
+        -- while preserving user modifications.
+        let target = dir </> "rpc.ts"
+        original <- TIO.readFile target
+        -- Add a user comment somewhere in the file
+        let userEdit = T.replace "// Generated" "// Generated\n// user custom line" original
+            fallback = original <> "\n// user custom line"
+            edited   = if userEdit /= original then userEdit else fallback
+        TIO.writeFile target edited
+
+        _ <- runAgain []
+
+        after <- TIO.readFile target
+        -- User modification must survive
+        T.isInfixOf "// user custom line" after `shouldBe` True
+
     -- ═══════════════════════════════════════════
+    -- Section 9b: additiveMerge unit tests
+    -- ═══════════════════════════════════════════
+    describe "additiveMerge" $ do
+
+      it "inserts a novel block after its anchor" $ do
+        let current = T.unlines ["line1", "line2", "line3"]
+            new     = T.unlines ["line1", "line2", "new_a", "new_b", "line3"]
+        case additiveMerge new current of
+          Nothing     -> expectationFailure "Expected Just, got Nothing"
+          Just merged -> do
+            T.isInfixOf "new_a" merged `shouldBe` True
+            T.isInfixOf "new_b" merged `shouldBe` True
+            T.isInfixOf "line2" merged `shouldBe` True
+            T.isInfixOf "line3" merged `shouldBe` True
+
+      it "handles multiple insertion blocks at different positions" $ do
+        let current = T.unlines ["a", "b", "c", "d"]
+            new     = T.unlines ["a", "x1", "b", "c", "x2", "d"]
+        case additiveMerge new current of
+          Nothing     -> expectationFailure "Expected Just, got Nothing"
+          Just merged -> do
+            T.isInfixOf "x1" merged `shouldBe` True
+            T.isInfixOf "x2" merged `shouldBe` True
+
+      it "handles novel lines at the beginning (before any shared line)" $ do
+        let current = T.unlines ["shared1", "shared2"]
+            new     = T.unlines ["brand_new", "shared1", "shared2"]
+        case additiveMerge new current of
+          Nothing     -> expectationFailure "Expected Just, got Nothing"
+          Just merged -> T.isInfixOf "brand_new" merged `shouldBe` True
+
+      it "returns current unchanged when no novel lines exist" $ do
+        let content = T.unlines ["a", "b", "c"]
+        additiveMerge content content `shouldBe` Just content
+
+      it "returns Nothing when files are too divergent" $ do
+        let current = T.unlines ["x", "y", "z"]
+            new     = T.unlines ["a", "b", "c", "d", "e"]
+        additiveMerge new current `shouldBe` Nothing
+
+      it "handles duplicate anchor lines via forward scanning" $ do
+        let current = T.unlines ["}", "code_a", "}", "code_b", "}"]
+            new     = T.unlines ["}", "code_a", "}", "inserted", "code_b", "}"]
+        case additiveMerge new current of
+          Nothing     -> expectationFailure "Expected Just, got Nothing"
+          Just merged -> T.isInfixOf "inserted" merged `shouldBe` True
+
+    -- ═══════════════════════════════════════════
     -- Section 10: synapse.config.json
     -- ═══════════════════════════════════════════
     describe "synapse.config.json" $ do
@@ -782,20 +848,28 @@
 
         it "rejects target with empty generate list" $ do
           let badTarget = TargetConfig
-                { tcGenerate  = []
-                , tcTransport = WsTransport
-                , tcOutputDir = "out"
-                , tcSmokePath = Nothing
+                { tcGenerate        = []
+                , tcTransport       = WsTransport
+                , tcOutputDir       = "out"
+                , tcSmokePath       = Nothing
+                , tcBackend         = Nothing
+                , tcUrl             = Nothing
+                , tcPlugins         = Nothing
+                , tcSharedTransport = Nothing
                 }
           let cfg = defaultSynapseConfig { scTargets = Map.singleton "t" badTarget }
           validateSynapseConfig cfg `shouldSatisfy` isLeft
 
         it "rejects target with empty outputDir" $ do
           let badTarget = TargetConfig
-                { tcGenerate  = ["plugins"]
-                , tcTransport = WsTransport
-                , tcOutputDir = ""
-                , tcSmokePath = Nothing
+                { tcGenerate        = ["plugins"]
+                , tcTransport       = WsTransport
+                , tcOutputDir       = ""
+                , tcSmokePath       = Nothing
+                , tcBackend         = Nothing
+                , tcUrl             = Nothing
+                , tcPlugins         = Nothing
+                , tcSharedTransport = Nothing
                 }
           let cfg = defaultSynapseConfig { scTargets = Map.singleton "t" badTarget }
           validateSynapseConfig cfg `shouldSatisfy` isLeft
@@ -979,7 +1053,7 @@
           Map.lookup "echo" (extractPluginHashesFromBytes ir) `shouldBe` Just ""
 
       describe "filterTargets" $ do
-        let mkTarget gen = TargetConfig gen WsTransport "out" Nothing
+        let mkTarget gen = TargetConfig gen WsTransport "out" Nothing Nothing Nothing Nothing Nothing
         let targets = Map.fromList
               [ ("client", mkTarget ["transport", "rpc", "plugins"])
               , ("smoke",  mkTarget ["smoke"])
@@ -1009,28 +1083,28 @@
         it "sets outputDir from target" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
           let sc  = defaultSynapseConfig { scUrl = Just "ws://localhost:4444" }
-          let tc  = TargetConfig ["plugins"] BrowserTransport "my/output/dir" Nothing
+          let tc  = TargetConfig ["plugins"] BrowserTransport "my/output/dir" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           optOutput (cfgOptions cfg) `shouldBe` "my/output/dir"
 
         it "sets transport from target" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
           let sc  = defaultSynapseConfig
-          let tc  = TargetConfig ["plugins"] BrowserTransport "out" Nothing
+          let tc  = TargetConfig ["plugins"] BrowserTransport "out" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           optTransport (cfgOptions cfg) `shouldBe` BrowserTransport
 
         it "parses host from URL" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
           let sc  = defaultSynapseConfig { scUrl = Just "ws://myhost:9000" }
-          let tc  = TargetConfig ["plugins"] WsTransport "out" Nothing
+          let tc  = TargetConfig ["plugins"] WsTransport "out" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           cfgHost cfg `shouldBe` "myhost"
 
         it "parses port from URL" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
           let sc  = defaultSynapseConfig { scUrl = Just "ws://localhost:9000" }
-          let tc  = TargetConfig ["plugins"] WsTransport "out" Nothing
+          let tc  = TargetConfig ["plugins"] WsTransport "out" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           cfgPort cfg `shouldBe` "9000"
 
@@ -1558,6 +1632,7 @@
         , optForce       = True
         , optTransport   = WsTransport
         }
+    , cfgPlugins = Nothing
     }
 
 -- | Call fidget.register via direct RPC.
