diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e6bd83498f..b97965066a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,7 +42,11 @@ jobs:
         run: |
           .github/setup-test-projects.sh &&\
           ./mill -i "bridges.scalajs-1[_].publishLocal" &&\
-          ./mill -i "bridges.scalajs-1[_].test"
+          ./mill -i "bridges.scala-native-04[_].publishLocal" &&\
+          ./mill -i "bridges.scala-native-05[_].publishLocal" &&\
+          ./mill -i "bridges.scalajs-1[_].test" &&\
+          ./mill -i "bridges.scala-native-04[_].test" &&\
+          ./mill -i "bridges.scala-native-05[_].test"
         shell: bash
 
   test:
@@ -67,8 +71,8 @@ jobs:
           .github/setup-test-projects.sh &&\
           ./mill -i 'backend[_].test.compile' &&\
           ./mill -i 'frontend[_].test.compile' &&\
-          ./mill -i 'backend[2.12.18].test' &&\
-          ./mill -i 'frontend[2.12.18].test'
+          ./mill -i 'backend[2.12.19].test' &&\
+          ./mill -i 'frontend[2.12.19].test'
         shell: bash
 
   jvm-tests:
diff --git a/.scalafmt.conf b/.scalafmt.conf
index 09ae845903..a0c6724130 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,4 +1,4 @@
-version = "3.7.15"
+version = "3.8.0"
 
 align.preset = more
 maxColumn = 100
diff --git a/backend/src/main/scala/bloop/ClientClassesObserver.scala b/backend/src/main/scala/bloop/ClientClassesObserver.scala
new file mode 100644
index 0000000000..e84810e5aa
--- /dev/null
+++ b/backend/src/main/scala/bloop/ClientClassesObserver.scala
@@ -0,0 +1,65 @@
+package bloop
+
+import java.io.File
+import java.util.concurrent.atomic.AtomicReference
+
+import scala.jdk.CollectionConverters._
+
+import bloop.io.AbsolutePath
+import bloop.task.Task
+
+import monix.reactive.Observable
+import monix.reactive.subjects.PublishSubject
+import sbt.internal.inc.PlainVirtualFileConverter
+import xsbti.VirtualFileRef
+import xsbti.compile.CompileAnalysis
+import xsbti.compile.analysis.Stamp
+
+/**
+ * Each time a new compile analysis is produced for a given client, it is given to
+ * the [[ClientClassObserver]] which computes the list of classes that changed or got created.
+ *
+ * A client can subscribe to the observer to get notified of classes to update.
+ * It is used by DAP to hot reload classes in the debuggee process.
+ *
+ * @param clientClassesDir the class directory for the client
+ */
+private[bloop] class ClientClassesObserver(val classesDir: AbsolutePath) {
+  private val converter = PlainVirtualFileConverter.converter
+  private val previousAnalysis: AtomicReference[CompileAnalysis] = new AtomicReference()
+  private val classesSubject: PublishSubject[Seq[String]] = PublishSubject()
+
+  def observable: Observable[Seq[String]] = classesSubject
+
+  def nextAnalysis(analysis: CompileAnalysis): Task[Unit] = {
+    val prev = previousAnalysis.getAndSet(analysis)
+    if (prev != null && classesSubject.size > 0) {
+      Task {
+        val previousStamps = prev.readStamps.getAllProductStamps
+        analysis.readStamps.getAllProductStamps.asScala.iterator.collect {
+          case (vf, stamp) if isClassFile(vf) && isNewer(stamp, previousStamps.get(vf)) =>
+            getFullyQualifiedClassName(vf)
+        }.toSeq
+      }
+        .flatMap { classesToUpdate =>
+          Task.fromFuture(classesSubject.onNext(classesToUpdate)).map(_ => ())
+        }
+    } else Task.unit
+  }
+
+  private def isClassFile(vf: VirtualFileRef): Boolean = vf.id.endsWith(".class")
+
+  private def isNewer(current: Stamp, previous: Stamp): Boolean =
+    previous == null || {
+      val currentHash = current.getHash
+      val previousHash = previous.getHash
+      currentHash.isPresent &&
+      (!previousHash.isPresent || currentHash.get != previousHash.get)
+    }
+
+  private def getFullyQualifiedClassName(vf: VirtualFileRef): String = {
+    val path = converter.toPath(vf)
+    val relativePath = classesDir.underlying.relativize(path)
+    relativePath.toString.replace(File.separator, ".").stripSuffix(".class")
+  }
+}
diff --git a/backend/src/main/scala/bloop/CompileBackgroundTasks.scala b/backend/src/main/scala/bloop/CompileBackgroundTasks.scala
index 41306919ae..5b4b57c5d2 100644
--- a/backend/src/main/scala/bloop/CompileBackgroundTasks.scala
+++ b/backend/src/main/scala/bloop/CompileBackgroundTasks.scala
@@ -8,7 +8,7 @@ import bloop.tracing.BraveTracer
 
 abstract class CompileBackgroundTasks {
   def trigger(
-      clientClassesDir: AbsolutePath,
+      clientClassesObserver: ClientClassesObserver,
       clientReporter: Reporter,
       clientTracer: BraveTracer,
       clientLogger: Logger
@@ -20,7 +20,7 @@ object CompileBackgroundTasks {
   val empty: CompileBackgroundTasks = {
     new CompileBackgroundTasks {
       def trigger(
-          clientClassesDir: AbsolutePath,
+          clientClassesObserver: ClientClassesObserver,
           clientReporter: Reporter,
           clientTracer: BraveTracer,
           clientLogger: Logger
diff --git a/backend/src/main/scala/bloop/Compiler.scala b/backend/src/main/scala/bloop/Compiler.scala
index 70f0d76f16..ab299e9fdb 100644
--- a/backend/src/main/scala/bloop/Compiler.scala
+++ b/backend/src/main/scala/bloop/Compiler.scala
@@ -40,6 +40,7 @@ import sbt.util.InterfaceUtil
 import xsbti.T2
 import xsbti.VirtualFileRef
 import xsbti.compile.{CompilerCache => _, ScalaInstance => _, _}
+import scala.util.Try
 
 case class CompileInputs(
     scalaInstance: ScalaInstance,
@@ -319,6 +320,7 @@ object Compiler {
     val classpathOptions = compileInputs.classpathOptions
     val compilers = compileInputs.compilerCache.get(
       scalaInstance,
+      classpathOptions,
       compileInputs.javacBin,
       compileInputs.javacOptions.toList
     )
@@ -451,11 +453,12 @@ object Compiler {
 
             val backgroundTasks = new CompileBackgroundTasks {
               def trigger(
-                  clientClassesDir: AbsolutePath,
+                  clientClassesObserver: ClientClassesObserver,
                   clientReporter: Reporter,
                   clientTracer: BraveTracer,
                   clientLogger: Logger
               ): Task[Unit] = Task.defer {
+                val clientClassesDir = clientClassesObserver.classesDir
                 clientLogger.debug(s"Triggering background tasks for $clientClassesDir")
                 val updateClientState =
                   updateExternalClassesDirWithReadOnly(clientClassesDir, clientTracer, clientLogger)
@@ -471,10 +474,20 @@ object Compiler {
                 }
 
                 val deleteNewClassesDir = Task(BloopPaths.delete(AbsolutePath(newClassesDir)))
-                val allTasks = List(deleteNewClassesDir, updateClientState, writeAnalysisIfMissing)
+                val publishClientAnalysis = Task {
+                  rebaseAnalysisClassFiles(
+                    analysis,
+                    readOnlyClassesDir,
+                    clientClassesDir.underlying,
+                    sourcesWithFatal
+                  )
+                }
+                  .flatMap(clientClassesObserver.nextAnalysis)
                 Task
-                  .gatherUnordered(allTasks)
-                  .map(_ => ())
+                  .gatherUnordered(
+                    List(deleteNewClassesDir, updateClientState, writeAnalysisIfMissing)
+                  )
+                  .flatMap(_ => publishClientAnalysis)
                   .onErrorHandleWith(err => {
                     clientLogger.debug("Caught error in background tasks"); clientLogger.trace(err);
                     Task.raiseError(err)
@@ -494,14 +507,12 @@ object Compiler {
             )
           } else {
             val allGeneratedProducts = allGeneratedRelativeClassFilePaths.toMap
-            val analysisForFutureCompilationRuns = {
-              rebaseAnalysisClassFiles(
-                analysis,
-                readOnlyClassesDir,
-                newClassesDir,
-                sourcesWithFatal
-              )
-            }
+            val analysisForFutureCompilationRuns = rebaseAnalysisClassFiles(
+              analysis,
+              readOnlyClassesDir,
+              newClassesDir,
+              sourcesWithFatal
+            )
 
             val resultForFutureCompilationRuns = {
               resultForDependentCompilationsInSameRun.withAnalysis(
@@ -516,12 +527,12 @@ object Compiler {
             // Schedule the tasks to run concurrently after the compilation end
             val backgroundTasksExecution = new CompileBackgroundTasks {
               def trigger(
-                  clientClassesDir: AbsolutePath,
+                  clientClassesObserver: ClientClassesObserver,
                   clientReporter: Reporter,
                   clientTracer: BraveTracer,
                   clientLogger: Logger
               ): Task[Unit] = {
-                val clientClassesDirPath = clientClassesDir.toString
+                val clientClassesDir = clientClassesObserver.classesDir
                 val successBackgroundTasks =
                   backgroundTasksWhenNewSuccessfulAnalysis
                     .map(f => f(clientClassesDir, clientReporter, clientTracer))
@@ -542,7 +553,7 @@ object Compiler {
                       val syntax = path.syntax
                       if (syntax.startsWith(readOnlyClassesDirPath)) {
                         val rebasedFile = AbsolutePath(
-                          syntax.replace(readOnlyClassesDirPath, clientClassesDirPath)
+                          syntax.replace(readOnlyClassesDirPath, clientClassesDir.toString)
                         )
                         if (rebasedFile.exists) {
                           Files.delete(rebasedFile.underlying)
@@ -550,7 +561,18 @@ object Compiler {
                       }
                     }
                   }
-                  Task.gatherUnordered(List(firstTask, secondTask)).map(_ => ())
+
+                  val publishClientAnalysis = Task {
+                    rebaseAnalysisClassFiles(
+                      analysis,
+                      newClassesDir,
+                      clientClassesDir.underlying,
+                      sourcesWithFatal
+                    )
+                  }.flatMap(clientClassesObserver.nextAnalysis)
+                  Task
+                    .gatherUnordered(List(firstTask, secondTask))
+                    .flatMap(_ => publishClientAnalysis)
                 }
 
                 allClientSyncTasks.doOnFinish(_ => Task(clientReporter.reportEndCompilation()))
@@ -632,25 +654,33 @@ object Compiler {
       case None => scalacOptions
       case Some(_) if existsReleaseSetting || sameHome => scalacOptions
       case Some(version) =>
-        try {
-          val numVer = if (version.startsWith("1.8")) 8 else version.takeWhile(_.isDigit).toInt
-          val bloopNumVer = JavaRuntime.version.takeWhile(_.isDigit).toInt
-          if (bloopNumVer > numVer) {
-            scalacOptions ++ List("-release", numVer.toString())
-          } else {
-            logger.warn(
-              s"Bloop is runing with ${JavaRuntime.version} but your code requires $version to compile, " +
-                "this might cause some compilation issues when using JDK API unsupported by the Bloop's current JVM version"
-            )
-            scalacOptions
+        val options: Option[Array[String]] =
+          for {
+            numVer <- parseJavaVersion(version)
+            bloopNumVer <- parseJavaVersion(JavaRuntime.version)
+            if (bloopNumVer >= 9 && numVer != bloopNumVer)
+          } yield {
+            if (bloopNumVer > numVer) {
+              scalacOptions ++ List("-release", numVer.toString())
+            } else {
+              logger.warn(
+                s"Bloop is running with ${JavaRuntime.version} but your code requires $version to compile, " +
+                  "this might cause some compilation issues when using JDK API unsupported by the Bloop's current JVM version"
+              )
+              scalacOptions
+            }
           }
-        } catch {
-          case NonFatal(_) =>
-            scalacOptions
-        }
+        options.getOrElse(scalacOptions)
     }
   }
 
+  private def parseJavaVersion(version: String): Option[Int] =
+    version.split('-').head.split('.').toList match {
+      case "1" :: minor :: _ => Try(minor.toInt).toOption
+      case single :: _ => Try(single.toInt).toOption
+      case _ => None
+    }
+
   private def getCompilationOptions(
       inputs: CompileInputs,
       logger: Logger,
@@ -688,11 +718,12 @@ object Compiler {
   ): CompileBackgroundTasks = {
     new CompileBackgroundTasks {
       def trigger(
-          clientClassesDir: AbsolutePath,
+          clientClassesObserver: ClientClassesObserver,
           clientReporter: Reporter,
           tracer: BraveTracer,
           clientLogger: Logger
       ): Task[Unit] = {
+        val clientClassesDir = clientClassesObserver.classesDir
         val backgroundTasks = tasks.map(f => f(clientClassesDir, clientReporter, tracer))
         Task.gatherUnordered(backgroundTasks).memoize.map(_ => ())
       }
@@ -780,8 +811,8 @@ object Compiler {
    */
   def rebaseAnalysisClassFiles(
       analysis0: CompileAnalysis,
-      readOnlyClassesDir: Path,
-      newClassesDir: Path,
+      origin: Path,
+      target: Path,
       sourceFilesWithFatalWarnings: scala.collection.Set[File]
   ): Analysis = {
     // Cast to the only internal analysis that we support
@@ -789,10 +820,10 @@ object Compiler {
     def rebase(file: VirtualFileRef): VirtualFileRef = {
 
       val filePath = converter.toPath(file).toAbsolutePath()
-      if (!filePath.startsWith(readOnlyClassesDir)) file
+      if (!filePath.startsWith(origin)) file
       else {
         // Hash for class file is the same because the copy duplicates metadata
-        val path = newClassesDir.resolve(readOnlyClassesDir.relativize(filePath))
+        val path = target.resolve(origin.relativize(filePath))
         converter.toVirtualFile(path)
       }
     }
diff --git a/backend/src/main/scala/bloop/CompilerCache.scala b/backend/src/main/scala/bloop/CompilerCache.scala
index 0bac7cb89b..9315c28cc3 100644
--- a/backend/src/main/scala/bloop/CompilerCache.scala
+++ b/backend/src/main/scala/bloop/CompilerCache.scala
@@ -35,10 +35,10 @@ import sbt.internal.util.LoggerWriter
 import xsbti.ComponentProvider
 import xsbti.VirtualFile
 import xsbti.compile.ClassFileManager
+import xsbti.compile.ClasspathOptions
 import xsbti.compile.Compilers
 import xsbti.compile.JavaCompiler
 import xsbti.compile.Output
-import xsbti.compile.ScalaCompiler
 import xsbti.{Logger => XLogger}
 import xsbti.{Reporter => XReporter}
 
@@ -50,19 +50,21 @@ final class CompilerCache(
     logger: Logger
 ) {
 
-  private val scalaCompilerCache = new ConcurrentHashMap[ScalaInstance, ScalaCompiler]()
-
+  private val scalaInstanceCache = new ConcurrentHashMap[ScalaInstance, ScalaInstance]()
   private val javaCompilerCache = new ConcurrentHashMap[JavacKey, JavaCompiler]()
 
   def get(
       scalaInstance: ScalaInstance,
+      classpathOptions: ClasspathOptions,
       javacBin: Option[AbsolutePath],
       javacOptions: List[String]
   ): Compilers = {
-    val scalaCompiler = scalaCompilerCache.computeIfAbsent(
+    val scalaInstanceFromCache = scalaInstanceCache.computeIfAbsent(
       scalaInstance,
-      getScalaCompiler(_, componentProvider)
+      _ => scalaInstance
     )
+    val scalaCompiler =
+      getScalaCompiler(scalaInstanceFromCache, classpathOptions, componentProvider)
 
     val allowLocal = !hasRuntimeJavacOptions(javacOptions)
     val javaCompiler =
@@ -108,12 +110,13 @@ final class CompilerCache(
 
   def getScalaCompiler(
       scalaInstance: ScalaInstance,
+      classpathOptions: ClasspathOptions,
       componentProvider: ComponentProvider
   ): AnalyzingCompiler = {
     val bridgeSources = BloopComponentCompiler.getModuleForBridgeSources(scalaInstance)
     val bridgeId = BloopComponentCompiler.getBridgeComponentId(bridgeSources, scalaInstance)
     componentProvider.component(bridgeId) match {
-      case Array(jar) => ZincUtil.scalaCompiler(scalaInstance, jar)
+      case Array(jar) => ZincUtil.scalaCompiler(scalaInstance, jar, classpathOptions)
       case _ =>
         BloopZincLibraryManagement.scalaCompiler(
           scalaInstance,
@@ -121,6 +124,7 @@ final class CompilerCache(
           componentProvider,
           Some(Paths.getCacheDirectory("bridge-cache").toFile),
           bridgeSources,
+          classpathOptions,
           logger
         )
     }
diff --git a/backend/src/main/scala/bloop/io/ParallelOps.scala b/backend/src/main/scala/bloop/io/ParallelOps.scala
index 4f2e79f87e..49b7709788 100644
--- a/backend/src/main/scala/bloop/io/ParallelOps.scala
+++ b/backend/src/main/scala/bloop/io/ParallelOps.scala
@@ -173,7 +173,7 @@ object ParallelOps {
           ()
         } catch {
           case NonFatal(t) =>
-            logger.report(
+            logger.error(
               s"Unexpected error when copying $originFile to $targetFile, you might need to restart the build server.",
               t
             )
diff --git a/backend/src/main/scala/bloop/logging/BloopLogger.scala b/backend/src/main/scala/bloop/logging/BloopLogger.scala
index e36aa182b3..e20ccc67d7 100644
--- a/backend/src/main/scala/bloop/logging/BloopLogger.scala
+++ b/backend/src/main/scala/bloop/logging/BloopLogger.scala
@@ -146,13 +146,6 @@ object BloopLogger {
   def default(name: String): BloopLogger =
     at(name, System.out, System.err, false, DebugFilter.All)
 
-  def prettyPrintException(t: Throwable): String = {
-    val sw = new java.io.StringWriter()
-    val pw = new java.io.PrintWriter(sw)
-    t.printStackTrace(pw)
-    sw.toString()
-  }
-
   private lazy val colorsRegex = "\u001b\\[[0-9;]*m".r
 
   /**
diff --git a/backend/src/main/scala/bloop/util/JavaRuntime.scala b/backend/src/main/scala/bloop/util/JavaRuntime.scala
index 403016ad8f..f354e3a97a 100644
--- a/backend/src/main/scala/bloop/util/JavaRuntime.scala
+++ b/backend/src/main/scala/bloop/util/JavaRuntime.scala
@@ -4,7 +4,9 @@ import javax.tools.JavaCompiler
 import javax.tools.ToolProvider
 
 import scala.annotation.nowarn
+import scala.collection.concurrent.TrieMap
 import scala.util.Failure
+import scala.util.Properties
 import scala.util.Try
 
 import bloop.io.AbsolutePath
@@ -13,8 +15,6 @@ import com.typesafe.config.ConfigException
 import com.typesafe.config.ConfigFactory
 import com.typesafe.config.ConfigParseOptions
 import com.typesafe.config.ConfigSyntax
-import scala.collection.concurrent.TrieMap
-import scala.util.Properties
 
 sealed trait JavaRuntime
 object JavaRuntime {
diff --git a/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala b/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala
index 973b442204..c01fdfd619 100644
--- a/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala
+++ b/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala
@@ -29,6 +29,7 @@ object BloopZincLibraryManagement {
       componentProvider: ComponentProvider,
       secondaryCacheDir: Option[File],
       compilerBridgeSource: ModuleID,
+      classpathOptions: ClasspathOptions,
       logger: _root_.bloop.logging.Logger
   ): AnalyzingCompiler = {
     val compilerBridgeProvider = BloopComponentCompiler.interfaceProvider(
@@ -40,7 +41,7 @@ object BloopZincLibraryManagement {
     new AnalyzingCompiler(
       scalaInstance,
       compilerBridgeProvider,
-      ClasspathOptionsUtil.boot(),
+      classpathOptions,
       _ => (),
       loader
     )
diff --git a/backend/src/test/scala/bloop/CompilerCacheSpec.scala b/backend/src/test/scala/bloop/CompilerCacheSpec.scala
index 29dcac41eb..806e026825 100644
--- a/backend/src/test/scala/bloop/CompilerCacheSpec.scala
+++ b/backend/src/test/scala/bloop/CompilerCacheSpec.scala
@@ -19,6 +19,7 @@ import sbt.internal.inc.BloopComponentCompiler
 import sbt.internal.inc.javac.WriteReportingJavaFileObject
 import sbt.io.syntax.File
 import xsbti.compile.ClassFileManager
+import xsbti.compile.ClasspathOptionsUtil
 
 @Category(Array(classOf[FastTests]))
 class CompilerCacheSpec {
@@ -104,12 +105,31 @@ class CompilerCacheSpec {
       bloop.internal.build.BloopScalaInfo.scalaVersion,
       new RecordingLogger()
     )
+    val cpOptions = ClasspathOptionsUtil.boot()
 
-    val javac0 = compilerCache.get(scalaInstance, None, Nil).javaTools().javac()
-    val javac1 = compilerCache.get(scalaInstance, None, Nil).javaTools().javac()
+    val javac0 = compilerCache.get(scalaInstance, cpOptions, None, Nil).javaTools().javac()
+    val javac1 = compilerCache.get(scalaInstance, cpOptions, None, Nil).javaTools().javac()
     assertTrue(javac0 + " was not eq to " + javac1, javac0 eq javac1)
   }
 
+  @Test
+  def differentClasspathOptions(): Unit = withCompilerCache { compilerCache =>
+    val scalaInstance = ScalaInstance.resolve(
+      "org.scala-lang",
+      "scala-compiler",
+      bloop.internal.build.BloopScalaInfo.scalaVersion,
+      new RecordingLogger()
+    )
+    val classpathOptions1 = ClasspathOptionsUtil.boot()
+    val classpathOptions2 = ClasspathOptionsUtil.manual()
+
+    val scalac1 = compilerCache.get(scalaInstance, classpathOptions1, None, Nil).scalac()
+    val scalac2 = compilerCache.get(scalaInstance, classpathOptions2, None, Nil).scalac()
+    assertEquals(scalac1.classpathOptions, classpathOptions1)
+    assertEquals(scalac2.classpathOptions, classpathOptions2)
+    assertEquals(scalac1.scalaInstance, scalac2.scalaInstance)
+  }
+
   @Test
   def runtimeOptionsNeverLocal(): Unit = withCompilerCache { compilerCache =>
     val scalaInstance = ScalaInstance.resolve(
@@ -118,13 +138,15 @@ class CompilerCacheSpec {
       bloop.internal.build.BloopScalaInfo.scalaVersion,
       new RecordingLogger()
     )
+    val cpOptions = ClasspathOptionsUtil.boot()
 
     // We first populate the compiler cache with a compiler that may be local.
-    val javac0 = compilerCache.get(scalaInstance, None, Nil).javaTools().javac()
-    val javac1 = compilerCache.get(scalaInstance, None, List("-J-Dfoo=bar")).javaTools().javac()
+    val _ = compilerCache.get(scalaInstance, cpOptions, None, Nil).javaTools().javac()
+    val javac1 =
+      compilerCache.get(scalaInstance, cpOptions, None, List("-J-Dfoo=bar")).javaTools().javac()
 
     assertTrue(
-      s"`javac1` was not a forked compiler, despite the runtime flag: ${javac0.getClass}",
+      s"`javac1` was not a forked compiler, despite the runtime flag: ${javac1.getClass}",
       javac1.isInstanceOf[compilerCache.BloopForkedJavaCompiler]
     )
   }
diff --git a/bloop-rifle/src/bloop/rifle/internal/Operations.scala b/bloop-rifle/src/bloop/rifle/internal/Operations.scala
index 81755d15bf..314e8c16fe 100644
--- a/bloop-rifle/src/bloop/rifle/internal/Operations.scala
+++ b/bloop-rifle/src/bloop/rifle/internal/Operations.scala
@@ -145,6 +145,7 @@ object Operations {
         val writeOutputToOpt0 =
           if (bloopServerSupportsFileTruncating) Some(s.outputPath)
           else None
+
         (Seq(s"daemon:${s.path}"), "bloop.Bloop", writeOutputToOpt0)
     }
 
diff --git a/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala b/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala
index 773d689a7d..2498e12517 100644
--- a/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala
+++ b/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala
@@ -40,11 +40,9 @@ class SnailgunClient(openSocket: () => Socket) extends snailgun.Client {
         case t: SocketException =>
           logger.debug("Tracing an ignored socket exception...")
           logger.trace(t)
-          ()
         case t: SocketExceptionLike =>
           logger.debug("Tracing an ignored socket exception-like...")
           logger.trace(t)
-          ()
       }
   }
 }
diff --git a/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala b/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala
index 2e3b4bb9be..3f29b296fe 100644
--- a/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala
+++ b/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala
@@ -37,6 +37,7 @@ object NativeBridge {
     }
     val nativeLTO = config.mode match {
       case LinkerMode.Debug => build.LTO.none
+      case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.full
       case LinkerMode.Release => build.LTO.thin
     }
 
diff --git a/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala b/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala
index 5d61cc8038..3733523393 100644
--- a/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala
+++ b/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala
@@ -53,7 +53,8 @@ class ScalaNativeToolchainSpec {
     val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3)
 
     assertTrue(s"Linking failed: ${logger.getMessages.mkString("\n")}", resultingState.status.isOk)
-    logger.getMessages.assertContain("Optimizing (release-full mode)", atLevel = "info")
+    // nothing is printed for fast release
+    logger.getMessages.assertContain("Generated native binary", atLevel = "info")
   }
 
   @Test def canRunScalaNativeProjectDefaultMainClass(): Unit = {
diff --git a/bridges/scala-native-0.5/src/main/scala/bloop/scalanative/NativeBridge.scala b/bridges/scala-native-0.5/src/main/scala/bloop/scalanative/NativeBridge.scala
new file mode 100644
index 0000000000..04627c5ecc
--- /dev/null
+++ b/bridges/scala-native-0.5/src/main/scala/bloop/scalanative/NativeBridge.scala
@@ -0,0 +1,102 @@
+package bloop.scalanative
+import java.nio.file.Files
+import java.nio.file.Path
+
+import scala.scalanative.build
+import scala.scalanative.util.Scope
+import scala.concurrent.Future
+import scala.concurrent.ExecutionContext
+
+import bloop.config.Config.LinkerMode
+import bloop.config.Config.NativeConfig
+import bloop.data.Project
+import bloop.io.Paths
+import bloop.logging.DebugFilter
+import bloop.logging.Logger
+
+object NativeBridge {
+  private implicit val ctx: DebugFilter = DebugFilter.Link
+  private val sharedScope = Scope.unsafe()
+
+  def nativeLink(
+      config0: NativeConfig,
+      project: Project,
+      classpath: Array[Path],
+      entry: String,
+      target: Path,
+      logger: Logger,
+      ec: ExecutionContext
+  ): Future[Path] = {
+    val workdir = project.out.resolve("native")
+    if (workdir.isDirectory) Paths.delete(workdir)
+    Files.createDirectories(workdir.underlying)
+
+    val nativeLogger =
+      build.Logger(logger.trace _, logger.debug _, logger.info _, logger.warn _, logger.error _)
+    val config = setUpNativeConfig(project, classpath, config0)
+    val nativeMode = config.mode match {
+      case LinkerMode.Debug => build.Mode.debug
+      case LinkerMode.Release => build.Mode.releaseFast
+    }
+    val nativeLTO = config.mode match {
+      case LinkerMode.Debug => build.LTO.none
+      case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.none
+      case LinkerMode.Release => build.LTO.thin
+    }
+
+    val nativeConfig =
+      build.Config.empty
+        .withMainClass(Option(entry))
+        .withClassPath(classpath)
+        .withBaseDir(target.getParent())
+        .withLogger(nativeLogger)
+        .withCompilerConfig(
+          build.NativeConfig.empty
+            .withClang(config.clang)
+            .withClangPP(config.clangpp)
+            .withBaseName(target.getFileName().toString())
+            .withCompileOptions(config.options.compiler)
+            .withLinkingOptions(config.options.linker)
+            .withGC(build.GC(config.gc))
+            .withMode(nativeMode)
+            .withLTO(nativeLTO)
+            .withLinkStubs(config.linkStubs)
+            .withCheck(config.check)
+            .withDump(config.dump)
+            .withTargetTriple(config.targetTriple)
+        )
+
+    build.Build.build(nativeConfig)(sharedScope, ec)
+  }
+
+  private[scalanative] def setUpNativeConfig(
+      project: Project,
+      classpath: Array[Path],
+      config: NativeConfig
+  ): NativeConfig = {
+    val mode = config.mode
+    val options = config.options
+    val gc = if (config.gc.isEmpty) build.GC.default.name else config.gc
+    val clang = if (config.clang.toString.isEmpty) build.Discover.clang() else config.clang
+    val clangpp = if (config.clangpp.toString.isEmpty) build.Discover.clangpp() else config.clangpp
+    val lopts = if (options.linker.isEmpty) build.Discover.linkingOptions() else options.linker
+    val copts = if (options.compiler.isEmpty) build.Discover.compileOptions() else options.compiler
+
+    val targetTriple = config.targetTriple
+
+    NativeConfig.apply(
+      version = config.version,
+      mode = mode,
+      toolchain = Nil, // No worries, toolchain is on this project's classpath
+      gc = gc,
+      targetTriple = targetTriple,
+      clang = clang,
+      clangpp = clangpp,
+      options = options,
+      linkStubs = config.linkStubs,
+      check = config.check,
+      dump = config.dump,
+      output = config.output
+    )
+  }
+}
diff --git a/bridges/scala-native-0.5/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala b/bridges/scala-native-0.5/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala
new file mode 100644
index 0000000000..11bd91719b
--- /dev/null
+++ b/bridges/scala-native-0.5/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala
@@ -0,0 +1,105 @@
+package bloop.scalanative
+
+import java.util.concurrent.TimeUnit
+
+import scala.concurrent.duration.Duration
+
+import bloop.cli.Commands
+import bloop.cli.OptimizerConfig
+import bloop.data.Platform
+import bloop.data.Project
+import bloop.engine.Run
+import bloop.engine.tasks.toolchains.ScalaNativeToolchain
+import bloop.logging.RecordingLogger
+import bloop.util.TestUtil
+
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.experimental.categories.Category
+import bloop.engine.State
+import bloop.config.Config
+
+@Category(Array(classOf[bloop.FastTests]))
+class ScalaNativeToolchainSpec {
+  private val state0 = {
+    def setUpNative(p: Project): Project = {
+      val platform = p.platform match {
+        case nativePlatform: Platform.Native =>
+          nativePlatform.copy(
+            toolchain = Some(ScalaNativeToolchain.apply(this.getClass.getClassLoader))
+          )
+
+        case _ => p.platform
+      }
+      p.copy(platform = platform)
+    }
+
+    val configDir = TestUtil.getBloopConfigDir("cross-test-build-scala-native-0.5")
+    val logger = bloop.logging.BloopLogger.default(configDir.toString())
+    TestUtil.loadTestProject(configDir, logger, true, _.map(setUpNative))
+  }
+  @Test def canLinkScalaNativeProject(): Unit = {
+    val logger = new RecordingLogger
+    val state = state0.copy(logger = logger)
+    val action = Run(Commands.Link(List("test-projectNative")))
+    val resultingState = TestUtil.blockingExecute(action, state, maxDuration)
+
+    assertTrue(s"Linking failed: ${logger.getMessages.mkString("\n")}", resultingState.status.isOk)
+    logger.getMessages.assertContain("Generated native binary '", atLevel = "info")
+  }
+
+  @Test def canLinkScalaNativeProjectInReleaseMode(): Unit = {
+    val logger = new RecordingLogger
+    val mode = OptimizerConfig.Release
+    val state = state0.copy(logger = logger)
+    val action = Run(Commands.Link(List("test-projectNative"), optimize = Some(mode)))
+    val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3)
+
+    assertTrue(
+      s"Linking failed: ${logger.getMessages.mkString("\n")}",
+      resultingState.status.isOk
+    )
+    logger.getMessages.assertContain("Optimizing (release-fast mode)", atLevel = "info")
+  }
+
+  @Test def canRunScalaNativeProjectDefaultMainClass(): Unit = {
+    val logger = new RecordingLogger
+    val state = state0.copy(logger = logger)
+    val action = Run(Commands.Run(List("test-projectNative")))
+    val resultingState = TestUtil.blockingExecute(action, state, maxDuration)
+
+    assertTrue(
+      s"Linking failed: ${logger.getMessages.mkString("\n")}",
+      resultingState.status.isOk
+    )
+    logger.getMessages.assertContain("Hello, world from DefaultApp!", atLevel = "info")
+  }
+
+  @Test def canRunScalaJvmProjectDefaultMainClass(): Unit = {
+    val logger = new RecordingLogger
+    val state = state0.copy(logger = logger)
+    val action = Run(Commands.Run(List("test-project"), main = None))
+    val resultingState = TestUtil.blockingExecute(action, state, maxDuration)
+
+    assertTrue(
+      s"Linking failed: ${logger.getMessages.mkString("\n")}",
+      resultingState.status.isOk
+    )
+    logger.getMessages.assertContain("Hello, world!", atLevel = "info")
+  }
+
+  private val maxDuration = Duration.apply(90, TimeUnit.SECONDS)
+  private implicit class RichLogs(logs: List[(String, String)]) {
+    def assertContain(needle: String, atLevel: String): Unit = {
+      def failMessage = s"""Logs did not contain `$needle` at level `$atLevel`. Logs were:
+                           |${logs.mkString("\n")}""".stripMargin
+      assertTrue(
+        failMessage,
+        logs.exists {
+          case (`atLevel`, msg) => msg.contains(needle)
+          case _ => false
+        }
+      )
+    }
+  }
+}
diff --git a/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala b/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala
index 86877f94d5..b189f22055 100644
--- a/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala
+++ b/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala
@@ -14,12 +14,12 @@ import bloop.config.Config.ModuleKindJS
 import bloop.data.Project
 import bloop.logging.DebugFilter
 import bloop.logging.{Logger => BloopLogger}
-import bloop.scalajs.jsenv.JsDomNodeJsEnv
-import bloop.scalajs.jsenv.NodeJSConfig
-import bloop.scalajs.jsenv.NodeJSEnv
 
 import org.scalajs.jsenv.Input
+import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv
+import org.scalajs.jsenv.nodejs.NodeJSEnv
 import org.scalajs.linker.PathIRContainer
+import org.scalajs.linker.PathOutputDirectory
 import org.scalajs.linker.PathOutputFile
 import org.scalajs.linker.StandardImpl
 import org.scalajs.linker.interface.{ModuleKind => ScalaJSModuleKind, _}
@@ -27,20 +27,10 @@ import org.scalajs.logging.Level
 import org.scalajs.logging.{Logger => JsLogger}
 import org.scalajs.testing.adapter.TestAdapter
 import org.scalajs.testing.adapter.TestAdapterInitializer
+import java.nio.file.Files
 
 /**
  * Defines operations provided by the Scala.JS 1.x toolchain.
- *
- * The 1.x js bridge needs to inline the implementation of `NodeJSEnv`,
- * `JSDOMNodeJSEnv` and `ComRunner` because there is a bug in the latest
- * Scala.js release that does not run `close` on the underlying process,
- * skipping the destruction of the process running Scala.js tests. Aside
- * from leaking, this is fatal in Windows because the underlying process
- * is alive and keeps open references to the output JS file.
- *
- * We can remove all of the js environments and runners as soon as this
- * issue is fixed upstream. Note that our 0.6.x version handles cancellation
- * correctly.
  */
 object JsBridge {
   private class Logger(logger: BloopLogger)(implicit filter: DebugFilter) extends JsLogger {
@@ -94,13 +84,13 @@ object JsBridge {
       classpath: Array[Path],
       runMain: java.lang.Boolean,
       mainClass: Option[String],
-      target: Path,
+      targetDirectory: Path,
       logger: BloopLogger,
       executionContext: ExecutionContext
   ): Unit = {
     implicit val ec = executionContext
     implicit val logFilter: DebugFilter = DebugFilter.Link
-    val linker = ScalaJSLinker.reuseOrCreate(config, target)
+    val linker = ScalaJSLinker.reuseOrCreate(config, targetDirectory)
 
     val cache = StandardImpl.irFileCache().newCache
     val irContainersPairs = PathIRContainer.fromClasspath(classpath)
@@ -126,11 +116,14 @@ object JsBridge {
         }
     }
 
-    val output = LinkerOutput(PathOutputFile(target))
-
     val resultFuture = for {
       libraryIRs <- libraryIrsFuture
-      _ <- linker.link(libraryIRs, moduleInitializers, output, new Logger(logger))
+      _ <- linker.link(
+        libraryIRs,
+        moduleInitializers,
+        PathOutputDirectory(targetDirectory),
+        new Logger(logger)
+      )
     } yield ()
 
     Await.result(resultFuture, Duration.Inf)
@@ -151,11 +144,12 @@ object JsBridge {
     if (nodeModules.toFile().exists()) {
       logger.debug("Node.js module path: " + nodeModules.toString())
       val fullEnv = Map("NODE_PATH" -> nodeModules.toString()) ++ env
-      val config =
-        NodeJSConfig().withExecutable(nodePath).withCwd(Some(baseDirectory)).withEnv(fullEnv)
       val nodeEnv =
-        if (!jsConfig.jsdom.contains(true)) new NodeJSEnv(logger, config)
-        else new JsDomNodeJsEnv(logger, config)
+        if (!jsConfig.jsdom.contains(true))
+          new NodeJSEnv(
+            NodeJSEnv.Config().withExecutable(nodePath).withEnv(fullEnv)
+          )
+        else new JSDOMNodeJSEnv(JSDOMNodeJSEnv.Config().withExecutable(nodePath).withEnv(fullEnv))
 
       // The order of the scripts mandates the load order in the JavaScript runtime
       val input = jsConfig.kind match {
@@ -169,7 +163,7 @@ object JsBridge {
       val result = adapter.loadFrameworks(frameworkNames).flatMap(_.toList)
       (result, () => adapter.close())
     } else {
-      logger.error(
+      logger.warn(
         s"Cannot discover test frameworks, missing node_modules in test project, expected them at $nodeModules"
       )
       (Nil, () => ())
diff --git a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/JsDomNodeJsEnv.scala b/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/JsDomNodeJsEnv.scala
deleted file mode 100644
index 1fd0ef1cfe..0000000000
--- a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/JsDomNodeJsEnv.scala
+++ /dev/null
@@ -1,198 +0,0 @@
-package bloop.scalajs.jsenv
-
-import java.io.File
-import java.io.InputStream
-import java.net.URI
-import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.StandardCopyOption
-
-import scala.util.control.NonFatal
-
-import bloop.logging.Logger
-
-import com.google.common.jimfs.Jimfs
-import org.scalajs.jsenv.ExternalJSRun
-import org.scalajs.jsenv.Input
-import org.scalajs.jsenv.JSComRun
-import org.scalajs.jsenv.JSEnv
-import org.scalajs.jsenv.JSRun
-import org.scalajs.jsenv.JSUtils.escapeJS
-import org.scalajs.jsenv.RunConfig
-import org.scalajs.jsenv.UnsupportedInputException
-import org.scalajs.jsenv.nodejs.BloopComRun
-
-/**
- * See comments in [[bloop.scalajs.JsBridge]].
- *
- * Adapted from `jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala`.
- */
-class JsDomNodeJsEnv(logger: Logger, config: NodeJSConfig) extends JSEnv {
-  private lazy val validator = ExternalJSRun.supports(RunConfig.Validator())
-
-  val name: String = "Node.js with jsdom"
-
-  def start(input: Seq[Input], runConfig: RunConfig): JSRun = {
-    JsDomNodeJsEnv.validator.validate(runConfig)
-    val scripts = validateInput(input)
-    try {
-      internalStart(codeWithJSDOMContext(scripts), runConfig)
-    } catch {
-      case NonFatal(t) =>
-        JSRun.failed(t)
-    }
-  }
-
-  def startWithCom(input: Seq[Input], runConfig: RunConfig, onMessage: String => Unit): JSComRun = {
-    validator.validate(runConfig)
-    val scripts = validateInput(input)
-    BloopComRun.start(runConfig, onMessage) { comLoader =>
-      internalStart(comLoader :: codeWithJSDOMContext(scripts), runConfig)
-    }
-  }
-
-  private def validateInput(input: Seq[Input]): List[Path] = {
-    input.map {
-      case Input.Script(script) =>
-        script
-
-      case _ =>
-        throw new UnsupportedInputException(input)
-    }.toList
-  }
-
-  private def internalStart(files: List[Path], runConfig: RunConfig): JSRun =
-    NodeJSEnv.internalStart(logger, config, env)(
-      NodeJSEnv.write(files.map(Input.Script)),
-      runConfig
-    )
-
-  private def env: Map[String, String] =
-    Map("NODE_MODULE_CONTEXTS" -> "0") ++ config.env
-
-  private def codeWithJSDOMContext(scripts: List[Path]): List[Path] = {
-    val scriptsURIs = scripts.map(JsDomNodeJsEnv.materialize)
-    val scriptsURIsAsJSStrings =
-      scriptsURIs.map(uri => "\"" + escapeJS(uri.toASCIIString) + "\"")
-    val scriptsURIsJSArray = scriptsURIsAsJSStrings.mkString("[", ", ", "]")
-    val jsDOMCode = {
-      s"""
-         |
-         |(function () {
-         |  var jsdom = require("jsdom");
-         |
-         |  if (typeof jsdom.JSDOM === "function") {
-         |    // jsdom >= 10.0.0
-         |    var virtualConsole = new jsdom.VirtualConsole()
-         |      .sendTo(console, { omitJSDOMErrors: true });
-         |    virtualConsole.on("jsdomError", function (error) {
-         |      try {
-         |        // Display as much info about the error as possible
-         |        if (error.detail && error.detail.stack) {
-         |          console.error("" + error.detail);
-         |          console.error(error.detail.stack);
-         |        } else {
-         |          console.error(error);
-         |        }
-         |      } finally {
-         |        // Whatever happens, kill the process so that the run fails
-         |        process.exit(1);
-         |      }
-         |    });
-         |
-         |    var dom = new jsdom.JSDOM("", {
-         |      virtualConsole: virtualConsole,
-         |      url: "http://localhost/",
-         |
-         |      /* Allow unrestricted <script> tags. This is exactly as
-         |       * "dangerous" as the arbitrary execution of script files we
-         |       * do in the non-jsdom Node.js env.
-         |       */
-         |      resources: "usable",
-         |      runScripts: "dangerously"
-         |    });
-         |
-         |    var window = dom.window;
-         |    window["scalajsCom"] = global.scalajsCom;
-         |
-         |    var scriptsSrcs = $scriptsURIsJSArray;
-         |    for (var i = 0; i < scriptsSrcs.length; i++) {
-         |      var script = window.document.createElement("script");
-         |      script.src = scriptsSrcs[i];
-         |      window.document.body.appendChild(script);
-         |    }
-         |  } else {
-         |    // jsdom v9.x
-         |    var virtualConsole = jsdom.createVirtualConsole()
-         |      .sendTo(console, { omitJsdomErrors: true });
-         |    virtualConsole.on("jsdomError", function (error) {
-         |      /* This inelegant if + console.error is the only way I found
-         |       * to make sure the stack trace of the original error is
-         |       * printed out.
-         |       */
-         |      if (error.detail && error.detail.stack)
-         |        console.error(error.detail.stack);
-         |
-         |      // Throw the error anew to make sure the whole execution fails
-         |      throw error;
-         |    });
-         |
-         |    jsdom.env({
-         |      html: "",
-         |      virtualConsole: virtualConsole,
-         |      url: "http://localhost/",
-         |      created: function (error, window) {
-         |        if (error == null) {
-         |          window["scalajsCom"] = global.scalajsCom;
-         |        } else {
-         |          throw error;
-         |        }
-         |      },
-         |      scripts: $scriptsURIsJSArray
-         |    });
-         |  }
-         |})();
-         |""".stripMargin
-    }
-    List(
-      Files.write(
-        Jimfs.newFileSystem().getPath("codeWithJSDOMContext.js"),
-        jsDOMCode.getBytes(StandardCharsets.UTF_8)
-      )
-    )
-  }
-}
-
-object JsDomNodeJsEnv {
-  private lazy val validator = ExternalJSRun.supports(RunConfig.Validator())
-
-  // tmpSuffixRE and tmpFile copied from HTMLRunnerBuilder.scala in Scala.js
-
-  private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r
-  private def tmpFile(path: String, in: InputStream): URI = {
-    try {
-      /* - createTempFile requires a prefix of at least 3 chars
-       * - we use a safe part of the path as suffix so the extension stays (some
-       *   browsers need that) and there is a clue which file it came from.
-       */
-      val suffix = tmpSuffixRE.findFirstIn(path).orNull
-
-      val f = File.createTempFile("tmp-", suffix)
-      f.deleteOnExit()
-      Files.copy(in, f.toPath, StandardCopyOption.REPLACE_EXISTING)
-      f.toURI
-    } finally {
-      in.close()
-    }
-  }
-
-  private def materialize(path: Path): URI = {
-    try {
-      path.toFile.toURI
-    } catch {
-      case _: UnsupportedOperationException =>
-        tmpFile(path.toString, Files.newInputStream(path))
-    }
-  }
-}
diff --git a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/NodeJsEnv.scala b/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/NodeJsEnv.scala
deleted file mode 100644
index 82e9a7a983..0000000000
--- a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/NodeJsEnv.scala
+++ /dev/null
@@ -1,308 +0,0 @@
-package bloop.scalajs.jsenv
-
-import java.io.File
-import java.io.OutputStream
-import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.StandardCopyOption
-
-import scala.concurrent.Future
-
-import bloop.engine.ExecutionContext
-import bloop.exec.Forker
-import bloop.logging.DebugFilter
-import bloop.logging.Logger
-
-import com.google.common.jimfs.Jimfs
-import monix.execution.Cancelable
-import monix.execution.atomic.AtomicBoolean
-import org.scalajs.jsenv.ExternalJSRun
-import org.scalajs.jsenv.Input
-import org.scalajs.jsenv.JSComRun
-import org.scalajs.jsenv.JSEnv
-import org.scalajs.jsenv.JSRun
-import org.scalajs.jsenv.JSUtils.escapeJS
-import org.scalajs.jsenv.RunConfig
-import org.scalajs.jsenv.UnsupportedInputException
-import org.scalajs.jsenv.nodejs.BloopComRun
-import org.scalajs.jsenv.nodejs.NodeJSEnv.SourceMap
-
-// Copy pasted verbatim from Scala.js source code, added withCwd()
-// Original file: scala-js/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala
-final class NodeJSConfig private (
-    val executable: String,
-    val args: List[String],
-    val env: Map[String, String],
-    val cwd: Option[Path],
-    val sourceMap: SourceMap
-) {
-  private def this() = {
-    this(
-      executable = "node",
-      args = Nil,
-      env = Map.empty,
-      cwd = None,
-      sourceMap = SourceMap.EnableIfAvailable
-    )
-  }
-
-  def withExecutable(executable: String): NodeJSConfig =
-    copy(executable = executable)
-
-  def withArgs(args: List[String]): NodeJSConfig =
-    copy(args = args)
-
-  def withEnv(env: Map[String, String]): NodeJSConfig =
-    copy(env = env)
-
-  def withCwd(cwd: Option[Path]): NodeJSConfig =
-    copy(cwd = cwd)
-
-  def withSourceMap(sourceMap: SourceMap): NodeJSConfig =
-    copy(sourceMap = sourceMap)
-
-  /** Forces enabling (true) or disabling (false) source maps. */
-  def withSourceMap(sourceMap: Boolean): NodeJSConfig =
-    withSourceMap(if (sourceMap) SourceMap.EnableIfAvailable else SourceMap.Disable)
-
-  private def copy(
-      executable: String = executable,
-      args: List[String] = args,
-      env: Map[String, String] = env,
-      cwd: Option[Path] = cwd,
-      sourceMap: SourceMap = sourceMap
-  ): NodeJSConfig = {
-    new NodeJSConfig(executable, args, env, cwd, sourceMap)
-  }
-}
-
-object NodeJSConfig {
-
-  /**
-   * Returns a default configuration for a [[jsenv.NodeJSEnv]].
-   *
-   *  The defaults are:
-   *
-   *  - `executable`: `"node"`
-   *  - `args`: `Nil`
-   *  - `env`: `Map.empty`
-   *  - `cwd`: `None`
-   *  - `sourceMap`: [[org.scalajs.jsenv.nodejs.NodeJSEnv.SourceMap.EnableIfAvailable]]
-   */
-  def apply(): NodeJSConfig = new NodeJSConfig()
-}
-
-/**
- * See comments in [[bloop.scalajs.JsBridge]].
- *
- * Adapted from `scala-js/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala`.
- */
-final class NodeJSEnv(logger: Logger, config: NodeJSConfig) extends JSEnv {
-  override val name: String = "Node.js"
-  private def env: Map[String, String] = Map("NODE_MODULE_CONTEXTS" -> "0") ++ config.env
-
-  def start(input: Seq[Input], runConfig: RunConfig): JSRun = {
-    NodeJSEnv.validator.validate(runConfig)
-    validateInput(input)
-    internalStart(initFiles ++ input, runConfig)
-  }
-
-  def startWithCom(input: Seq[Input], runConfig: RunConfig, onMessage: String => Unit): JSComRun = {
-    NodeJSEnv.validator.validate(runConfig)
-    validateInput(input)
-    BloopComRun.start(runConfig, onMessage) { comLoader =>
-      internalStart(initFiles ++ (Input.Script(comLoader) +: input), runConfig)
-    }
-  }
-
-  private def validateInput(input: Seq[Input]): Unit = input.foreach {
-    case _: Input.Script | _: Input.ESModule | _: Input.CommonJSModule =>
-    // ok
-    case _ =>
-      throw new UnsupportedInputException(input)
-  }
-
-  private def internalStart(input: Seq[Input], runConfig: RunConfig): JSRun = {
-    runConfig.logger.debug("Using input file: " + (input.mkString(",")))
-    NodeJSEnv.internalStart(logger, config, env)(NodeJSEnv.write(input), runConfig)
-  }
-
-  import NodeJSEnv.installSourceMap
-  import NodeJSEnv.installSourceMapIfAvailable
-
-  private def initFiles: Seq[Input] = config.sourceMap match {
-    case SourceMap.Disable => Nil
-    case SourceMap.EnableIfAvailable => Input.Script(installSourceMapIfAvailable) :: Nil
-    case SourceMap.Enable => Input.Script(installSourceMap) :: Nil
-  }
-}
-
-object NodeJSEnv {
-  private lazy val fs = Jimfs.newFileSystem()
-  implicit val debugFilter: DebugFilter = DebugFilter.Test
-  private lazy val validator = ExternalJSRun.supports(RunConfig.Validator())
-
-  private lazy val installSourceMapIfAvailable = {
-    Files.write(
-      fs.getPath("optionalSourceMapSupport.js"),
-      """
-        |try {
-        |  require('source-map-support').install();
-        |} catch (e) {
-        |};
-        """.stripMargin.getBytes(StandardCharsets.UTF_8)
-    )
-  }
-
-  private lazy val installSourceMap = {
-    Files.write(
-      fs.getPath("sourceMapSupport.js"),
-      "require('source-map-support').install();".getBytes(StandardCharsets.UTF_8)
-    )
-  }
-
-  private[jsenv] def write(input: Seq[Input])(out: OutputStream): Unit = {
-    def runScript(path: Path): String = {
-      try {
-        val f = path.toFile
-        val pathJS = "\"" + escapeJS(f.getAbsolutePath) + "\""
-        s"""
-          require('vm').runInThisContext(
-            require('fs').readFileSync($pathJS, { encoding: "utf-8" }),
-            { filename: $pathJS, displayErrors: true }
-          )
-        """
-      } catch {
-        case _: UnsupportedOperationException =>
-          val code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8)
-          val codeJS = "\"" + escapeJS(code) + "\""
-          val pathJS = "\"" + escapeJS(path.toString) + "\""
-          s"""
-            require('vm').runInThisContext(
-              $codeJS,
-              { filename: $pathJS, displayErrors: true }
-            )
-          """
-      }
-    }
-
-    def requireCommonJSModule(module: Path): String =
-      s"""require("${escapeJS(toFile(module).getAbsolutePath)}")"""
-
-    def importESModule(module: Path): String =
-      s"""import("${escapeJS(toFile(module).toURI.toASCIIString)}")"""
-
-    def execInputExpr(input: Input): String = input match {
-      case Input.Script(script) => runScript(script)
-      case Input.CommonJSModule(module) => requireCommonJSModule(module)
-      case Input.ESModule(module) => importESModule(module)
-    }
-
-    def println(str: String): Unit = {
-      out.write((str + "\n").getBytes("UTF-8"))
-      out.flush()
-    }
-
-    if (!input.exists(_.isInstanceOf[Input.ESModule])) {
-      /* If there is no ES module in the input, we can do everything
-       * synchronously, and directly on the standard input.
-       */
-      for (item <- input)
-        println(execInputExpr(item) + ";")
-    } else {
-      /* If there is at least one ES module, we must asynchronous chain things,
-       * and we must use an actual file to feed code to Node.js (because
-       * `import()` cannot be used from the standard input).
-       */
-      val importChain = input.foldLeft("Promise.resolve()") { (prev, item) =>
-        s"$prev.\n  then(${execInputExpr(item)})"
-      }
-      val importerFileContent = {
-        s"""
-           |$importChain.catch(e => {
-           |  console.error(e);
-           |  process.exit(1);
-           |});
-        """.stripMargin
-      }
-      val f = createTmpFile("importer.js")
-      Files.write(f.toPath, importerFileContent.getBytes(StandardCharsets.UTF_8))
-      println(s"""require("${escapeJS(f.getAbsolutePath)}");""")
-    }
-  }
-
-  private def toFile(path: Path): File = {
-    try {
-      path.toFile
-    } catch {
-      case _: UnsupportedOperationException =>
-        val f = createTmpFile(path.toString)
-        Files.copy(path, f.toPath, StandardCopyOption.REPLACE_EXISTING)
-        f
-    }
-  }
-
-  // tmpSuffixRE and createTmpFile copied from HTMLRunnerBuilder.scala
-
-  private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r
-
-  private def createTmpFile(path: String): File = {
-    /* - createTempFile requires a prefix of at least 3 chars
-     * - we use a safe part of the path as suffix so the extension stays (some
-     *   browsers need that) and there is a clue which file it came from.
-     */
-    val suffix = tmpSuffixRE.findFirstIn(path).orNull
-
-    val f = File.createTempFile("tmp-", suffix)
-    f.deleteOnExit()
-    f
-  }
-
-  def internalStart(
-      logger: Logger,
-      config: NodeJSConfig,
-      env: Map[String, String]
-  )(
-      write: OutputStream => Unit,
-      runConfig: RunConfig
-  ): JSRun = {
-    val command = config.executable :: config.args
-    logger.debug(s"Starting process ${command.mkString(" ")}...")
-    logger.debug(s"Current working directory: ${config.cwd}")
-    logger.debug(s"Current environment: ${config.env}")
-
-    val cancellable =
-      Forker
-        .run(
-          config.cwd.map(_.toFile),
-          command,
-          logger,
-          config.env,
-          writeToStdIn = outputStream => {
-            write(outputStream)
-            Cancelable.empty
-          },
-          debugLog = msg => logger.debug(msg)
-        )
-        .runAsync(ExecutionContext.ioScheduler)
-
-    new JSRun {
-      import scala.concurrent.ExecutionContext.Implicits.global
-
-      private val isClosed = AtomicBoolean(false)
-      override def future: Future[Unit] = cancellable.map { results =>
-        logger.debug(s"Finished with results: ${results}")
-        ()
-      }
-      override def close(): Unit = {
-        // Make sure we only destroy the process once, the test adapter can call this several times!
-        if (!isClosed.getAndSet(true)) {
-          logger.debug(s"Destroying process...")
-          cancellable.cancel()
-        }
-        ()
-      }
-    }
-  }
-}
diff --git a/bridges/scalajs-1/src/main/scala/org/scalajs/jsenv/nodejs/BloopComRun.scala b/bridges/scalajs-1/src/main/scala/org/scalajs/jsenv/nodejs/BloopComRun.scala
deleted file mode 100644
index 25381ca79a..0000000000
--- a/bridges/scalajs-1/src/main/scala/org/scalajs/jsenv/nodejs/BloopComRun.scala
+++ /dev/null
@@ -1,330 +0,0 @@
-/*                     __                                               *\
-**     ________ ___   / /  ___      __ ____  Scala.js Node.js env       **
-**    / __/ __// _ | / /  / _ | __ / // __/  (c) 2017, LAMP/EPFL        **
-**  __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \    http://scala-js.org/       **
-** /____/\___/_/ |_/____/_/ | |__/ /____/                               **
-**                          |/____/                                     **
-\*                                                                      */
-
-package org.scalajs.jsenv.nodejs
-
-import java.io._
-import java.net._
-import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import java.nio.file.Path
-
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent._
-import scala.util.Failure
-import scala.util.Success
-import scala.util.control.NonFatal
-
-import com.google.common.jimfs.Jimfs
-import org.scalajs.jsenv._
-
-/**
- * A copy of the `ComRun` in the scala-js codebase to handles
- * cancellation gracefully, as the Scala.js 0.6.x runner does.
- *
- * The runner has the following changes:
- *
- * 1. Invoke `run.close()` in `def close()`
- * 2. Only invoke the logic in `def close()` once as the call-site
- *    calls it more than once.
- */
-private final class BloopComRun(
-    run: JSRun,
-    handleMessage: String => Unit,
-    serverSocket: ServerSocket
-) extends JSComRun {
-  import BloopComRun._
-
-  /** Promise that completes once the receiver thread is completed. */
-  private[this] val promise = Promise[Unit]()
-
-  @volatile
-  private[this] var state: State = AwaitingConnection(Nil)
-
-  // If the run completes, make sure we also complete.
-  run.future.onComplete {
-    case Failure(t) => forceClose(t)
-    case Success(_) => onJSTerminated()
-  }
-
-  // TODO replace this with scheduled tasks on the execution context.
-  private[this] val receiver = new Thread {
-    setName("ComRun receiver")
-
-    override def run(): Unit = {
-      try {
-        try {
-          /* We need to await the connection unconditionally. Otherwise the JS end
-           * might try to connect indefinitely.
-           */
-          awaitConnection()
-
-          while (state != Closing) {
-            state match {
-              case s: AwaitingConnection =>
-                throw new IllegalStateException(s"Unexpected state: $s")
-
-              case Closing =>
-              /* We can end up here if there is a race between the two read to
-               * state. Do nothing, loop will terminate.
-               */
-
-              case Connected(_, _, js2jvm) =>
-                try {
-                  val len = js2jvm.readInt()
-                  val carr = Array.fill(len)(js2jvm.readChar())
-                  handleMessage(String.valueOf(carr))
-                } catch {
-                  case _: EOFException =>
-                    // JS end terminated gracefully. Close.
-                    close()
-                }
-            }
-          }
-        } catch {
-          case _: IOException if state == Closing =>
-          // We got interrupted by a graceful close.
-          // This is OK.
-        }
-
-        /* Everything got closed. We wait for the run to terminate.
-         * We need to wait in order to make sure that closing the
-         * underlying run does not fail it.
-         */
-        BloopComRun.this.run.future.foreach { _ =>
-          BloopComRun.this.run.close()
-          promise.trySuccess(())
-        }
-      } catch {
-        case t: Throwable => handleThrowable(t)
-      }
-    }
-  }
-
-  receiver.start()
-
-  def future: Future[Unit] = promise.future
-
-  def send(msg: String): Unit = synchronized {
-    state match {
-      case AwaitingConnection(msgs) =>
-        state = AwaitingConnection(msg :: msgs)
-
-      case Connected(_, jvm2js, _) =>
-        try {
-          writeMsg(jvm2js, msg)
-          jvm2js.flush()
-        } catch {
-          case t: Throwable => handleThrowable(t)
-        }
-
-      case Closing => // ignore msg.
-    }
-  }
-
-  // ++ bloop
-  def close(): Unit = synchronized {
-    val oldState = state
-    if (oldState == Closing) ()
-    else {
-      // Close the underlying run
-      run.close()
-
-      // Signal receiver thread that it is OK if socket read fails.
-      state = Closing
-
-      oldState match {
-        case c: Connected =>
-          // Interrupts the receiver thread and signals the VM to terminate.
-          closeAll(c)
-
-        case Closing | _: AwaitingConnection =>
-      }
-    }
-  }
-  // -- bloop
-
-  private def onJSTerminated() = {
-    close()
-
-    /* Interrupt receiver if we are still waiting for connection.
-     * Should only be relevant if we are still awaiting the connection.
-     * Note: We cannot do this in close(), otherwise if the JVM side closes
-     * before the JS side connected, the JS VM will fail instead of terminate
-     * normally.
-     */
-    serverSocket.close()
-  }
-
-  private def forceClose(cause: Throwable) = {
-    promise.tryFailure(cause)
-    close()
-    run.close()
-    serverSocket.close()
-  }
-
-  private def handleThrowable(cause: Throwable) = {
-    forceClose(cause)
-    if (!NonFatal(cause))
-      throw cause
-  }
-
-  private def awaitConnection(): Unit = {
-    var comSocket: Socket = null
-    var jvm2js: DataOutputStream = null
-    var js2jvm: DataInputStream = null
-
-    try {
-      comSocket = serverSocket.accept()
-      serverSocket.close() // we don't need it anymore.
-      jvm2js = new DataOutputStream(new BufferedOutputStream(comSocket.getOutputStream))
-      js2jvm = new DataInputStream(new BufferedInputStream(comSocket.getInputStream))
-
-      onConnected(Connected(comSocket, jvm2js, js2jvm))
-    } catch {
-      case t: Throwable =>
-        closeAll(comSocket, jvm2js, js2jvm)
-        throw t
-    }
-  }
-
-  private def onConnected(c: Connected): Unit = synchronized {
-    state match {
-      case AwaitingConnection(msgs) =>
-        msgs.reverse.foreach(writeMsg(c.jvm2js, _))
-        c.jvm2js.flush()
-        state = c
-
-      case _: Connected =>
-        throw new IllegalStateException(s"Unexpected state: $state")
-
-      case Closing =>
-        closeAll(c)
-    }
-  }
-}
-
-object BloopComRun {
-
-  /**
-   * Starts a [[JSComRun]] using the provided [[JSRun]] launcher.
-   *
-   *  @param config Configuration for the run.
-   *  @param onMessage callback upon message reception.
-   *  @param startRun [[JSRun]] launcher. Gets passed a
-   *      [[java.nio.file.Path Path]] that initializes `scalaJSCom` on
-   *      `global`. Requires Node.js libraries.
-   */
-  def start(config: RunConfig, onMessage: String => Unit)(
-      startRun: Path => JSRun
-  ): JSComRun = {
-    try {
-      val serverSocket =
-        new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address
-
-      val run = startRun(setupFile(serverSocket.getLocalPort))
-
-      new BloopComRun(run, onMessage, serverSocket)
-    } catch {
-      case NonFatal(t) =>
-        println(s"GOT exception when running BloopComRun.start ${t}")
-        JSComRun.failed(t)
-    }
-  }
-
-  private def closeAll(c: Closeable*): Unit =
-    c.withFilter(_ != null).foreach(_.close())
-
-  private def closeAll(c: Connected): Unit =
-    closeAll(c.comSocket, c.jvm2js, c.js2jvm)
-
-  private sealed trait State
-
-  private final case class AwaitingConnection(sendQueue: List[String]) extends State
-
-  private final case class Connected(
-      comSocket: Socket,
-      jvm2js: DataOutputStream,
-      js2jvm: DataInputStream
-  ) extends State
-
-  private final case object Closing extends State
-
-  private def writeMsg(s: DataOutputStream, msg: String): Unit = {
-    s.writeInt(msg.length)
-    s.writeChars(msg)
-  }
-
-  private def setupFile(port: Int): Path = {
-    Files.write(
-      Jimfs.newFileSystem().getPath("comSetup.js"),
-      s"""
-         |(function() {
-         |  // The socket for communication
-         |  var socket = require('net').connect($port);
-         |
-         |  // Buffers received data
-         |  var inBuffer = Buffer.alloc(0);
-         |
-         |  // Buffers received messages
-         |  var inMessages = [];
-         |
-         |  // The callback where received messages go
-         |  var onMessage = null;
-         |
-         |  socket.on('data', function(data) {
-         |    inBuffer = Buffer.concat([inBuffer, data]);
-         |
-         |    while (inBuffer.length >= 4) {
-         |      var msgLen = inBuffer.readInt32BE(0);
-         |      var byteLen = 4 + msgLen * 2;
-         |
-         |      if (inBuffer.length < byteLen) return;
-         |      var res = "";
-         |
-         |      for (var i = 0; i < msgLen; ++i)
-         |        res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2));
-         |
-         |      inBuffer = inBuffer.slice(byteLen);
-         |
-         |      if (inMessages !== null) inMessages.push(res);
-         |      else onMessage(res);
-         |    }
-         |  });
-         |
-         |  socket.on('error', function(err) {
-         |    console.error("Scala.js Com failed: " + err);
-         |    process.exit(-1);
-         |  });
-         |
-         |  socket.on('close', function() { process.exit(0); });
-         |
-         |  global.scalajsCom = {
-         |    init: function(onMsg) {
-         |      if (onMessage !== null) throw new Error("Com already initialized");
-         |      onMessage = onMsg;
-         |      process.nextTick(function() {
-         |        for (var i = 0; i < inMessages.length; ++i)
-         |          onMessage(inMessages[i]);
-         |        inMessages = null;
-         |      });
-         |    },
-         |    send: function(msg) {
-         |      var len = msg.length;
-         |      var buf = Buffer.allocUnsafe(4 + len * 2);
-         |      buf.writeInt32BE(len, 0);
-         |      for (var i = 0; i < len; ++i)
-         |        buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2);
-         |      socket.write(buf);
-         |    }
-         |  }
-         |}).call(this);
-        """.stripMargin.getBytes(StandardCharsets.UTF_8)
-    )
-  }
-}
diff --git a/build.sc b/build.sc
index 39d3f97d42..3d64c217ba 100644
--- a/build.sc
+++ b/build.sc
@@ -16,26 +16,26 @@ import java.io.File
 import scala.concurrent.duration.{Duration, DurationInt}
 
 object Dependencies {
-  def scala212 = "2.12.18"
-  def scala213 = "2.13.12"
+  def scala212 = "2.12.19"
+  def scala213 = "2.13.13"
 
   def scalaVersions = Seq(scala212, scala213)
 
   def serverScalaVersion = scala212
 
-  def asmVersion         = "9.6"
+  def asmVersion         = "9.7"
   def coursierVersion    = "2.1.0-M6-53-gb4f448130"
   def graalvmVersion     = "22.2.0"
   def jsoniterVersion    = "2.13.3.2"
-  def scalaJs1Version    = "1.14.0"
+  def scalaJs1Version    = "1.16.0"
   def scalaJsEnvsVersion = "1.1.1"
 
   def asm               = ivy"org.ow2.asm:asm:$asmVersion"
   def asmUtil           = ivy"org.ow2.asm:asm-util:$asmVersion"
   def bloopConfig       = ivy"ch.epfl.scala::bloop-config:1.5.5"
-  def brave             = ivy"io.zipkin.brave:brave:5.16.0"
-  def bsp4j             = ivy"ch.epfl.scala:bsp4j:2.1.0-M7"
-  def bsp4s             = ivy"ch.epfl.scala::bsp4s:2.1.0-M7"
+  def brave             = ivy"io.zipkin.brave:brave:5.18.1"
+  def bsp4j             = ivy"ch.epfl.scala:bsp4j:2.1.1"
+  def bsp4s             = ivy"ch.epfl.scala::bsp4s:2.1.1"
   def caseApp           = ivy"com.github.alexarchambault::case-app:2.0.6"
   def caseApp21         = ivy"com.github.alexarchambault::case-app:2.1.0-M15"
   def collectionCompat  = ivy"org.scala-lang.modules::scala-collection-compat:2.9.0"
@@ -54,33 +54,34 @@ object Dependencies {
   def junit                  = ivy"com.github.sbt:junit-interface:0.13.3"
   def libdaemonjvm           = ivy"io.github.alexarchambault.libdaemon::libdaemon:0.0.11"
   def libraryManagement      = ivy"org.scala-sbt::librarymanagement-ivy:1.9.3"
-  def log4j                  = ivy"org.apache.logging.log4j:log4j-core:2.21.1"
-  def logback                = ivy"ch.qos.logback:logback-classic:1.4.6"
+  def log4j                  = ivy"org.apache.logging.log4j:log4j-core:2.23.0"
+  def logback                = ivy"ch.qos.logback:logback-classic:1.4.14"
   def macroParadise          = ivy"org.scalamacros:::paradise:2.1.1"
   def monix                  = ivy"io.monix::monix:3.2.0"
   def munit                  = ivy"org.scalameta::munit:0.7.29"
   def nailgun                = ivy"io.github.alexarchambault.bleep:nailgun-server:1.0.7"
   def osLib                  = ivy"com.lihaoyi::os-lib:0.9.0"
   def pprint                 = ivy"com.lihaoyi::pprint:0.8.1"
-  def sbtTestAgent           = ivy"org.scala-sbt:test-agent:1.9.7"
+  def sbtTestAgent           = ivy"org.scala-sbt:test-agent:1.9.9"
   def sbtTestInterface       = ivy"org.scala-sbt:test-interface:1.0"
-  def scalaDebugAdapter      = ivy"ch.epfl.scala::scala-debug-adapter:3.1.4"
+  def scalaDebugAdapter      = ivy"ch.epfl.scala::scala-debug-adapter:4.0.3"
   def scalaJsLinker1         = ivy"org.scala-js::scalajs-linker:$scalaJs1Version"
   def scalaJsEnvs1           = ivy"org.scala-js::scalajs-js-envs:$scalaJsEnvsVersion"
   def scalaJsEnvNode1        = ivy"org.scala-js::scalajs-env-nodejs:$scalaJsEnvsVersion"
   def scalaJsEnvJsdomNode1   = ivy"org.scala-js::scalajs-env-jsdom-nodejs:1.1.0"
   def scalaJsSbtTestAdapter1 = ivy"org.scala-js::scalajs-sbt-test-adapter:$scalaJs1Version"
   def scalaJsLogging1        = ivy"org.scala-js::scalajs-logging:1.1.1"
-  def scalaNativeTools04     = ivy"org.scala-native::tools:0.4.16"
-  def scalazCore             = ivy"org.scalaz::scalaz-core:7.3.7"
+  def scalaNativeTools04     = ivy"org.scala-native::tools:0.4.17"
+  def scalaNativeTools05     = ivy"org.scala-native::tools:0.5.0-RC2"
+  def scalazCore             = ivy"org.scalaz::scalaz-core:7.3.8"
   def snailgun      = ivy"io.github.alexarchambault.scala-cli.snailgun::snailgun-core:0.4.1-sc2"
   def sourcecode    = ivy"com.lihaoyi::sourcecode:0.3.1"
   def svm           = ivy"org.graalvm.nativeimage:svm:$graalvmVersion"
   def utest         = ivy"com.lihaoyi::utest:0.8.2"
   def xxHashLibrary = ivy"net.jpountz.lz4:lz4:1.3.0"
   def zinc          = ivy"org.scala-sbt::zinc:1.9.5"
-  def zipkinSender  = ivy"io.zipkin.reporter2:zipkin-sender-urlconnection:2.16.4"
-  def zt            = ivy"org.zeroturnaround:zt-zip:1.16"
+  def zipkinSender  = ivy"io.zipkin.reporter2:zipkin-sender-urlconnection:2.17.2"
+  def zt            = ivy"org.zeroturnaround:zt-zip:1.17"
 
   def graalVmId = s"graalvm-java17:$graalvmVersion"
 }
@@ -401,6 +402,7 @@ class Frontend(val crossScalaVersion: String) extends BloopCrossSbtModule with B
          |  def zincVersion = "${Dependencies.zinc.dep.version}"
          |  def snailgunVersion = "0.4.1-sc2"
          |  def nativeBridge04 = "${bridges.`scala-native-04`().artifactId()}"
+         |  def nativeBridge05 = "${bridges.`scala-native-05`().artifactId()}"
          |  def jsBridge1 = "${bridges.`scalajs-1`().artifactId()}"
          |}
          |""".stripMargin
@@ -539,14 +541,19 @@ object bridges extends Module {
   }
 
   object `scala-native-04` extends Cross[ScalaNative04](Dependencies.scalaVersions: _*)
-  class ScalaNative04(val crossScalaVersion: String)
+  object `scala-native-05` extends Cross[ScalaNative05](Dependencies.scalaVersions: _*)
+
+  class ScalaNative04(val crossScalaVersion: String) extends ScalaNative(crossScalaVersion, 4)
+  class ScalaNative05(val crossScalaVersion: String) extends ScalaNative(crossScalaVersion, 5)
+
+  class ScalaNative(val crossScalaVersion: String, nativeMajorVersion: Int)
       extends BloopCrossSbtModule
       with BloopPublish {
-    def artifactName = "bloop-native-bridge-0-4"
+    def artifactName = s"bloop-native-bridge-0-$nativeMajorVersion"
 
     private def updateSources(originalSources: Seq[PathRef]): Seq[PathRef] =
-      if (millSourcePath.endsWith(os.rel / "scala-native-04")) {
-        val updatedSourcePath = millSourcePath / os.up / "scala-native-0.4"
+      if (millSourcePath.endsWith(os.rel / s"scala-native-0$nativeMajorVersion")) {
+        val updatedSourcePath = millSourcePath / os.up / s"scala-native-0.$nativeMajorVersion"
         originalSources.map {
           case pathRef if pathRef.path.startsWith(millSourcePath) =>
             PathRef(updatedSourcePath / pathRef.path.relativeTo(millSourcePath))
@@ -563,9 +570,10 @@ object bridges extends Module {
       shared(),
       backend()
     )
-    def compileIvyDeps = super.compileIvyDeps() ++ Agg(
-      Dependencies.scalaNativeTools04
-    )
+    def compileIvyDeps = super.compileIvyDeps() ++ Agg {
+      if (nativeMajorVersion == 4) Dependencies.scalaNativeTools04
+      else Dependencies.scalaNativeTools05
+    }
 
     object test extends Tests {
       def sources = T.sources(updateSources(super.sources()))
diff --git a/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala b/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala
index 634d8b0e9b..7c0550b121 100644
--- a/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala
+++ b/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala
@@ -60,9 +60,9 @@ import bloop.internal.build.BuildInfo
 import bloop.io.AbsolutePath
 import bloop.io.Environment.lineSeparator
 import bloop.io.RelativePath
-import bloop.logging.BloopLogger
 import bloop.logging.BspServerLogger
 import bloop.logging.DebugFilter
+import bloop.logging.Logger
 import bloop.reporter.BspProjectReporter
 import bloop.reporter.ProblemPerPhase
 import bloop.reporter.ReporterConfig
@@ -139,6 +139,7 @@ final class BloopBspServices(
     .requestAsync(endpoints.BuildTarget.scalaMainClasses)(p => schedule(scalaMainClasses(p)))
     .requestAsync(endpoints.BuildTarget.scalaTestClasses)(p => schedule(scalaTestClasses(p)))
     .requestAsync(endpoints.BuildTarget.dependencySources)(p => schedule(dependencySources(p)))
+    .requestAsync(endpoints.BuildTarget.dependencyModules)(p => schedule(dependencyModules(p)))
     .requestAsync(endpoints.DebugSession.start)(p => schedule(startDebugSession(p)))
     .requestAsync(endpoints.BuildTarget.jvmTestEnvironment)(p => schedule(jvmTestEnvironment(p)))
     .requestAsync(endpoints.BuildTarget.jvmRunEnvironment)(p => schedule(jvmRunEnvironment(p)))
@@ -308,7 +309,7 @@ final class BloopBspServices(
               debugProvider = Some(BloopBspServices.DefaultDebugProvider),
               inverseSourcesProvider = Some(true),
               dependencySourcesProvider = Some(true),
-              dependencyModulesProvider = None,
+              dependencyModulesProvider = Some(true),
               resourcesProvider = Some(true),
               outputPathsProvider = None,
               buildTargetChangedProvider = Some(false),
@@ -558,7 +559,7 @@ final class BloopBspServices(
           Tasks.clean(state, projectsToClean, includeDeps = false).materialize.map {
             case Success(state) => (state, Right(bsp.CleanCacheResult(None, cleaned = true)))
             case Failure(exception) =>
-              val t = BloopLogger.prettyPrintException(exception)
+              val t = Logger.prettyPrintException(exception)
               val msg = s"Unexpected error when cleaning build targets!${lineSeparator}$t"
               state -> Right(bsp.CleanCacheResult(Some(msg), cleaned = false))
           }
@@ -602,10 +603,7 @@ final class BloopBspServices(
       params: bsp.DebugSessionParams
   ): BspEndpointResponse[bsp.DebugSessionAddress] = {
 
-    def inferDebuggee(
-        projects: Seq[Project],
-        state: State
-    ): BspResponse[Debuggee] = {
+    def inferDebuggee(projects: Seq[Project], state: State): BspResponse[Debuggee] = {
       def convert[A: JsonValueCodec](
           f: A => Either[String, Debuggee]
       ): Either[Response.Error, Debuggee] = {
@@ -646,7 +644,7 @@ final class BloopBspServices(
             BloopDebuggeeRunner.forTestSuite(projects, testClasses, state, ioScheduler)
           })
         case Some(bsp.DebugSessionParamsDataKind.ScalaAttachRemote) =>
-          Right(BloopDebuggeeRunner.forAttachRemote(state, ioScheduler, projects))
+          Right(BloopDebuggeeRunner.forAttachRemote(projects, state, ioScheduler))
         case dataKind => Left(Response.invalidRequest(s"Unsupported data kind: $dataKind"))
       }
     }
@@ -901,11 +899,12 @@ final class BloopBspServices(
                 }
             case platform @ Platform.Js(config, _, _) =>
               val cmd = Commands.Run(List(project.name))
-              val target = ScalaJsToolchain.linkTargetFrom(project, config)
-              linkMainWithJs(cmd, project, state, mainClass.className, target, platform)
+              val targetDir = ScalaJsToolchain.linkTargetFrom(project, config)
+              linkMainWithJs(cmd, project, state, mainClass.className, targetDir, platform)
                 .flatMap { state =>
+                  val files = targetDir.list.map(_.toString())
                   // We use node to run the program (is this a special case?)
-                  val args = ("node" +: target.syntax +: cmd.args).toArray
+                  val args = ("node" +: files ::: cmd.args).toArray
                   if (!state.status.isOk) Task.now(state)
                   else Tasks.runNativeOrJs(state, cwd, args)
                 }
@@ -1205,6 +1204,58 @@ final class BloopBspServices(
     }
   }
 
+  def dependencyModules(
+      request: bsp.DependencyModulesParams
+  ): BspEndpointResponse[bsp.DependencyModulesResult] = {
+    def modules(
+        projects: Seq[ProjectMapping],
+        state: State
+    ): BspResult[bsp.DependencyModulesResult] = {
+      val response = bsp.DependencyModulesResult(
+        projects.iterator.map {
+          case (target, project) =>
+            val modules = project.resolution.toList.flatMap { res =>
+              res.modules.map { module =>
+                val mavenDependencyModule = bsp.MavenDependencyModule(
+                  module.organization,
+                  module.name,
+                  module.version,
+                  module.artifacts.map(artifact =>
+                    bsp.MavenDependencyModuleArtifact(
+                      bsp.Uri(AbsolutePath(artifact.path).toBspUri),
+                      artifact.classifier
+                    )
+                  ),
+                  None
+                )
+
+                val encoded = writeToArray(mavenDependencyModule)
+                bsp.DependencyModule(
+                  module.name,
+                  module.version,
+                  Some(bsp.DependencyModuleDataKind.Maven),
+                  Some(RawJson(encoded))
+                )
+              }
+            }.distinct
+            bsp.DependencyModulesItem(target, modules)
+        }.toList
+      )
+
+      Task.now((state, Right(response)))
+    }
+
+    ifInitialized(None) { (state: State, logger: BspServerLogger) =>
+      mapToProjects(request.targets, state) match {
+        case Left(error) =>
+          // Log the mapping error to the user via a log event + an error status code
+          logger.error(error)
+          Task.now((state, Right(bsp.DependencyModulesResult(Nil))))
+        case Right(mappings) => modules(mappings, state)
+      }
+    }
+  }
+
   def dependencySources(
       request: bsp.DependencySourcesParams
   ): BspEndpointResponse[bsp.DependencySourcesResult] = {
diff --git a/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala b/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala
index 8d56ce2fc9..75fa303592 100644
--- a/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala
+++ b/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala
@@ -34,13 +34,12 @@ class BloopDebugToolsResolver(logger: Logger) extends DebugToolsResolver {
     }
   }
 
-  override def resolveStepFilter(scalaVersion: ScalaVersion): Try[ClassLoader] = {
-    getOrTryUpdate(stepFilterCache, scalaVersion) {
-      val stepFilterModule = s"${BuildInfo.scala3StepFilterName}_${scalaVersion.binaryVersion}"
-      val stepFilter = Artifact(BuildInfo.organization, stepFilterModule, BuildInfo.version)
-      val tastyCore = Artifact("org.scala-lang", "tasty-core_3", scalaVersion.value)
+  override def resolveDecoder(scalaVersion: ScalaVersion): Try[ClassLoader] = {
+    getOrTryUpdate(decoderCache, scalaVersion) {
+      val decoderModule = s"${BuildInfo.decoderName}_${scalaVersion.binaryVersion}"
+      val artifact = Artifact(BuildInfo.organization, decoderModule, BuildInfo.version)
       DependencyResolution
-        .resolveWithErrors(List(stepFilter, tastyCore), logger)
+        .resolveWithErrors(List(artifact), logger)
         .map(jars => toClassLoader(jars, true))
         .toTry
     }
@@ -66,5 +65,5 @@ class BloopDebugToolsResolver(logger: Logger) extends DebugToolsResolver {
 
 object BloopDebugToolsResolver {
   private val expressionCompilerCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty
-  private val stepFilterCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty
+  private val decoderCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty
 }
diff --git a/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala b/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala
index b7614c72de..0f4dbf92b0 100644
--- a/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala
+++ b/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala
@@ -1,6 +1,9 @@
 package bloop.dap
 
+import java.io.Closeable
+
 import scala.collection.mutable
+import scala.concurrent.Future
 
 import ch.epfl.scala.bsp
 import ch.epfl.scala.bsp.ScalaMainClass
@@ -15,29 +18,38 @@ import bloop.engine.Dag
 import bloop.engine.State
 import bloop.engine.tasks.RunMode
 import bloop.engine.tasks.Tasks
+import bloop.io.AbsolutePath
 import bloop.task.Task
 import bloop.testing.DebugLoggingEventHandler
 import bloop.testing.TestInternals
 
+import monix.execution.Ack
 import monix.execution.Scheduler
+import monix.reactive.Observable
 
 abstract class BloopDebuggee(
     initialState: State,
-    ioScheduler: Scheduler,
-    debugeeScalaVersion: Option[String]
-) extends Debuggee {
-
+    classUpdates: Observable[Seq[String]]
+)(implicit ioScheduler: Scheduler)
+    extends Debuggee {
+  protected def scalaVersionOpt: Option[String]
   // The version doesn't matter for project without Scala version (Java only)
-  val scalaVersion = ScalaVersion(debugeeScalaVersion.getOrElse("2.13.8"))
+  override val scalaVersion = ScalaVersion(scalaVersionOpt.getOrElse("2.13.8"))
 
   override def run(listener: DebuggeeListener): CancelableFuture[Unit] = {
     val debugSessionLogger = new DebuggeeLogger(listener, initialState.logger)
 
     val task = start(initialState.copy(logger = debugSessionLogger), listener)
       .map { status =>
-        if (!status.isOk) throw new Exception(s"debugee failed with ${status.name}")
+        if (!status.isOk) throw new Exception(s"debuggee failed with ${status.name}")
       }
-    DapCancellableFuture.runAsync(task, ioScheduler)
+    DapCancellableFuture.runAsync(task)
+  }
+
+  override def observeClassUpdates(onClassUpdate: Seq[String] => Unit): Closeable = {
+    val subscription =
+      classUpdates.subscribe(onClassUpdate.andThen(_ => Future.successful(Ack.Continue)))
+    () => subscription.cancel()
   }
 
   protected def start(state: State, listener: DebuggeeListener): Task[ExitStatus]
@@ -49,11 +61,14 @@ private final class MainClassDebugAdapter(
     val modules: Seq[Module],
     val libraries: Seq[Library],
     val unmanagedEntries: Seq[UnmanagedEntry],
+    val classUpdates: Observable[Seq[String]],
     env: JdkConfig,
     initialState: State,
-    ioScheduler: Scheduler,
-    scalaVersion: Option[String]
-) extends BloopDebuggee(initialState, ioScheduler, scalaVersion) {
+    ioScheduler: Scheduler
+) extends BloopDebuggee(initialState, classUpdates)(ioScheduler) {
+
+  protected def scalaVersionOpt: Option[String] = project.scalaInstance.map(_.version)
+
   val javaRuntime: Option[JavaRuntime] = JavaRuntime(env.javaHome.underlying)
   def name: String = s"${getClass.getSimpleName}(${project.name}, ${mainClass.className})"
   def start(state: State, listener: DebuggeeListener): Task[ExitStatus] = {
@@ -83,10 +98,14 @@ private final class TestSuiteDebugAdapter(
     val libraries: Seq[Library],
     val unmanagedEntries: Seq[UnmanagedEntry],
     val javaRuntime: Option[JavaRuntime],
+    val classUpdates: Observable[Seq[String]],
     initialState: State,
-    ioScheduler: Scheduler,
-    val debugeeScalaVersion: Option[String]
-) extends BloopDebuggee(initialState, ioScheduler, debugeeScalaVersion) {
+    ioScheduler: Scheduler
+) extends BloopDebuggee(initialState, classUpdates)(ioScheduler) {
+
+  protected def scalaVersionOpt: Option[String] =
+    projects.headOption.flatMap(_.scalaInstance.map(_.version))
+
   override def name: String = {
     val projectsStr = projects.map(_.bspUri).mkString("[", ", ", "]")
     val selectedTests = testClasses.suites
@@ -116,14 +135,19 @@ private final class TestSuiteDebugAdapter(
 }
 
 private final class AttachRemoteDebugAdapter(
+    projects: Seq[Project],
     val modules: Seq[Module],
     val libraries: Seq[Library],
     val unmanagedEntries: Seq[UnmanagedEntry],
     val javaRuntime: Option[JavaRuntime],
+    val classUpdates: Observable[Seq[String]],
     initialState: State,
-    ioScheduler: Scheduler,
-    val debugeeScalaVersion: Option[String]
-) extends BloopDebuggee(initialState, ioScheduler, debugeeScalaVersion) {
+    ioScheduler: Scheduler
+) extends BloopDebuggee(initialState, classUpdates)(ioScheduler) {
+
+  protected def scalaVersionOpt: Option[String] =
+    projects.headOption.flatMap(_.scalaInstance.map(_.version))
+
   override def name: String = s"${getClass.getSimpleName}(${initialState.build.origin})"
   override def start(state: State, listener: DebuggeeListener): Task[ExitStatus] = Task(
     ExitStatus.Ok
@@ -131,7 +155,6 @@ private final class AttachRemoteDebugAdapter(
 }
 
 object BloopDebuggeeRunner {
-
   def forMainClass(
       projects: Seq[Project],
       mainClass: ScalaMainClass,
@@ -143,11 +166,8 @@ object BloopDebuggeeRunner {
       case Seq(project) =>
         project.platform match {
           case jvm: Platform.Jvm =>
-            val dag = state.build.getDagFor(project)
-            val modules = getModules(dag, state.client)
-            val libraries = getLibraries(dag)
-            val unmanagedEntries =
-              getUnmanagedEntries(project, dag, state.client, modules ++ libraries)
+            val (modules, libraries, unmanagedEntries, classUpdates) =
+              getEntriesAndClassUpdates(project, state)
             Right(
               new MainClassDebugAdapter(
                 project,
@@ -155,10 +175,10 @@ object BloopDebuggeeRunner {
                 modules,
                 libraries,
                 unmanagedEntries,
-                jvm.config,
+                classUpdates,
+                jvm.runtimeConfig.getOrElse(jvm.config),
                 state,
-                ioScheduler,
-                project.scalaInstance.map(_.version)
+                ioScheduler
               )
             )
           case platform =>
@@ -180,12 +200,10 @@ object BloopDebuggeeRunner {
           s"No projects specified for the test suites: [${testClasses.suites.map(_.className).sorted}]"
         )
       case Seq(project) if project.platform.isInstanceOf[Platform.Jvm] =>
-        val dag = state.build.getDagFor(project)
-        val modules = getModules(dag, state.client)
-        val libraries = getLibraries(dag)
-        val unmanagedEntries = getUnmanagedEntries(project, dag, state.client, modules ++ libraries)
-        val Platform.Jvm(config, _, _, _, _, _) = project.platform
-        val javaRuntime = JavaRuntime(config.javaHome.underlying)
+        val (modules, libraries, unmanagedEntries, classUpdates) =
+          getEntriesAndClassUpdates(project, state)
+        val Platform.Jvm(config, _, _, runtimeConfig, _, _) = project.platform
+        val javaRuntime = JavaRuntime(runtimeConfig.getOrElse(config).javaHome.underlying)
         Right(
           new TestSuiteDebugAdapter(
             projects,
@@ -194,13 +212,13 @@ object BloopDebuggeeRunner {
             libraries,
             unmanagedEntries,
             javaRuntime,
+            classUpdates,
             state,
-            ioScheduler,
-            project.scalaInstance.map(_.version)
+            ioScheduler
           )
         )
 
-      case project :: _ =>
+      case _ =>
         Right(
           new TestSuiteDebugAdapter(
             projects,
@@ -209,9 +227,9 @@ object BloopDebuggeeRunner {
             Seq.empty,
             Seq.empty,
             None,
+            Observable.empty,
             state,
-            ioScheduler,
-            project.scalaInstance.map(_.version)
+            ioScheduler
           )
         )
 
@@ -219,43 +237,57 @@ object BloopDebuggeeRunner {
   }
 
   def forAttachRemote(
+      projects: Seq[Project],
       state: State,
-      ioScheduler: Scheduler,
-      projects: Seq[Project]
+      ioScheduler: Scheduler
   ): Debuggee = {
     projects match {
       case Seq(project) if project.platform.isInstanceOf[Platform.Jvm] =>
-        val dag = state.build.getDagFor(project)
-        val libraries = getLibraries(dag)
-        val modules = getModules(dag, state.client)
-        val unmanagedEntries = getUnmanagedEntries(project, dag, state.client, modules ++ libraries)
-        val Platform.Jvm(config, _, _, _, _, _) = project.platform
-        val javaRuntime = JavaRuntime(config.javaHome.underlying)
+        val (modules, libraries, unmanagedEntries, classUpdates) =
+          getEntriesAndClassUpdates(project, state)
+        val Platform.Jvm(config, _, _, runtimeConfig, _, _) = project.platform
+        val javaRuntime = JavaRuntime(runtimeConfig.getOrElse(config).javaHome.underlying)
         new AttachRemoteDebugAdapter(
+          Seq(project),
           modules,
           libraries,
           unmanagedEntries,
           javaRuntime,
+          classUpdates,
           state,
-          ioScheduler,
-          project.scalaInstance.map(_.version)
+          ioScheduler
         )
       case projects =>
         new AttachRemoteDebugAdapter(
+          projects,
           Seq.empty,
           Seq.empty,
           Seq.empty,
           None,
+          Observable.empty,
           state,
-          ioScheduler,
-          projects.headOption.flatMap(_.scalaInstance).map(_.version)
+          ioScheduler
         )
     }
   }
 
-  private def getLibraries(dag: Dag[Project]): Seq[Library] = {
-    Dag
-      .dfs(dag, mode = Dag.PreOrder)
+  private def getEntriesAndClassUpdates(
+      project: Project,
+      state: State
+  ): (Seq[Module], Seq[Library], Seq[UnmanagedEntry], Observable[Seq[String]]) = {
+    val dag = state.build.getDagFor(project)
+    val projects = Dag.dfs(dag, mode = Dag.PreOrder)
+    val modules = getModules(projects, state.client)
+    val libraries = getLibraries(projects)
+    val fullClasspath = project.fullClasspath(dag, state.client)
+    val unmanagedEntries = getUnmanagedEntries(fullClasspath, modules ++ libraries)
+    val allClassUpdates = projects.map(state.client.getClassesObserverFor(_).observable)
+    val mergedClassUpdates = Observable.fromIterable(allClassUpdates).merge
+    (modules, libraries, unmanagedEntries, mergedClassUpdates)
+  }
+
+  private def getLibraries(projects: Seq[Project]): Seq[Library] = {
+    projects
       .flatMap(_.resolution)
       .flatMap(_.modules)
       .distinct
@@ -272,20 +304,16 @@ object BloopDebuggeeRunner {
   }
 
   private def getUnmanagedEntries(
-      project: Project,
-      dag: Dag[Project],
-      client: ClientInfo,
+      fullClasspath: Seq[AbsolutePath],
       managedEntries: Seq[ManagedEntry]
   ): Seq[UnmanagedEntry] = {
     val managedPaths = managedEntries.map(_.absolutePath).toSet
-    val fullClasspath = project.fullClasspath(dag, client).map(_.underlying).toSeq
     fullClasspath
-      .filter(p => !managedPaths.contains(p))
-      .map(UnmanagedEntry.apply)
+      .collect { case p if !managedPaths.contains(p.underlying) => UnmanagedEntry(p.underlying) }
   }
 
-  private def getModules(dag: Dag[Project], client: ClientInfo): Seq[Module] = {
-    Dag.dfs(dag, mode = Dag.PreOrder).map { project =>
+  private def getModules(projects: Seq[Project], client: ClientInfo): Seq[Module] = {
+    projects.map { project =>
       val sourceBuffer = mutable.Buffer.empty[SourceEntry]
       for (sourcePath <- project.sources) {
         if (sourcePath.isDirectory) {
diff --git a/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala b/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala
index 70e0acc643..70ed8e7e1d 100644
--- a/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala
+++ b/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala
@@ -4,6 +4,8 @@ import scala.concurrent.Future
 import scala.concurrent.Promise
 import scala.util.Success
 
+import ch.epfl.scala.debugadapter.CancelableFuture
+
 import bloop.task.Task
 
 import monix.execution.Cancelable
@@ -16,10 +18,7 @@ private class DapCancellableFuture(future: Future[Unit], cancelable: Cancelable)
 }
 
 object DapCancellableFuture {
-  def runAsync(
-      task: Task[Unit],
-      ioScheduler: Scheduler
-  ): ch.epfl.scala.debugadapter.CancelableFuture[Unit] = {
+  def runAsync(task: Task[Unit])(implicit ioScheduler: Scheduler): CancelableFuture[Unit] = {
     val promise = Promise[Unit]()
     val cancelable = task
       .doOnFinish {
diff --git a/frontend/src/main/scala/bloop/data/ClientInfo.scala b/frontend/src/main/scala/bloop/data/ClientInfo.scala
index 53de37989e..478985dc6f 100644
--- a/frontend/src/main/scala/bloop/data/ClientInfo.scala
+++ b/frontend/src/main/scala/bloop/data/ClientInfo.scala
@@ -15,6 +15,7 @@ import bloop.io.AbsolutePath
 import bloop.io.Filenames
 import bloop.io.Paths
 import bloop.util.UUIDUtil
+import bloop.ClientClassesObserver
 
 sealed trait ClientInfo {
 
@@ -40,6 +41,18 @@ sealed trait ClientInfo {
    */
   def getUniqueClassesDirFor(project: Project, forceGeneration: Boolean): AbsolutePath
 
+  /**
+   * Provides the classes observer for a given project. One can subscribe to it
+   * to get notified when some classes change or get created.
+   * It is used by DAP to hot reload classes in the debuggee process.
+   */
+  private val classesObserver = new ConcurrentHashMap[Project, ClientClassesObserver]()
+  def getClassesObserverFor(project: Project): ClientClassesObserver =
+    classesObserver.computeIfAbsent(
+      project,
+      project => new ClientClassesObserver(getUniqueClassesDirFor(project, true))
+    )
+
   /**
    * Tells the caller whether this client manages its own client classes
    * directories or whether bloop should take care of any created resources.
diff --git a/frontend/src/main/scala/bloop/data/Project.scala b/frontend/src/main/scala/bloop/data/Project.scala
index fb91f04654..cabb75d403 100644
--- a/frontend/src/main/scala/bloop/data/Project.scala
+++ b/frontend/src/main/scala/bloop/data/Project.scala
@@ -85,7 +85,7 @@ final case class Project(
       case _ =>
         None
     }
-    customWorkingDirectory.getOrElse(baseDirectory)
+    customWorkingDirectory.orElse(workspaceDirectory).getOrElse(baseDirectory)
   }
 
   def allGeneratorInputs: Task[List[AbsolutePath]] =
diff --git a/frontend/src/main/scala/bloop/engine/Interpreter.scala b/frontend/src/main/scala/bloop/engine/Interpreter.scala
index d48ea75239..c4a32e8e18 100644
--- a/frontend/src/main/scala/bloop/engine/Interpreter.scala
+++ b/frontend/src/main/scala/bloop/engine/Interpreter.scala
@@ -139,7 +139,7 @@ object Interpreter {
         }
 
         if (!bloop.util.CrossPlatform.isWindows)
-          state.logger.info("\u001b[H\u001b[2J")
+          state.logger.info(bloop.util.Console.clearCommand)
 
         // Force the first execution before relying on the file watching task
         fg(state).flatMap(newState => watcher.watch(newState, fg))
@@ -521,8 +521,8 @@ object Interpreter {
                 LinkTask.linkMainWithNative(cmd, project, state, mainClass, target, platform)
 
               case platform @ Platform.Js(config, _, _) =>
-                val target = ScalaJsToolchain.linkTargetFrom(project, config)
-                LinkTask.linkMainWithJs(cmd, project, state, mainClass, target, platform)
+                val targetDirectory = ScalaJsToolchain.linkTargetFrom(project, config)
+                LinkTask.linkMainWithJs(cmd, project, state, mainClass, targetDirectory, platform)
 
               case _: Platform.Jvm =>
                 val msg = Feedback.noLinkFor(project)
@@ -565,14 +565,16 @@ object Interpreter {
                     else Tasks.runNativeOrJs(state, cwd, args)
                   }
               case platform @ Platform.Js(config, _, _) =>
-                val target = ScalaJsToolchain.linkTargetFrom(project, config)
-                LinkTask.linkMainWithJs(cmd, project, state, mainClass, target, platform).flatMap {
-                  state =>
+                val targetDirectory = ScalaJsToolchain.linkTargetFrom(project, config)
+                LinkTask
+                  .linkMainWithJs(cmd, project, state, mainClass, targetDirectory, platform)
+                  .flatMap { state =>
                     // We use node to run the program (is this a special case?)
-                    val args = ("node" +: target.syntax +: cmd.args).toArray
+                    val files = targetDirectory.list.map(_.toString())
+                    val args = ("node" +: files ::: cmd.args).toArray
                     if (!state.status.isOk) Task.now(state)
                     else Tasks.runNativeOrJs(state, cwd, args)
-                }
+                  }
               case jvm: Platform.Jvm =>
                 val javaEnv = project.runtimeJdkConfig.getOrElse(jvm.config)
                 Tasks.runJVM(
diff --git a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala
index 4a198343d1..19e20f4096 100644
--- a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala
+++ b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala
@@ -22,7 +22,6 @@ import bloop.engine.tasks.compilation._
 import bloop.io.ParallelOps
 import bloop.io.ParallelOps.CopyMode
 import bloop.io.{Paths => BloopPaths}
-import bloop.logging.BloopLogger
 import bloop.logging.DebugFilter
 import bloop.logging.Logger
 import bloop.logging.LoggerAction
@@ -37,6 +36,7 @@ import bloop.tracing.BraveTracer
 import monix.execution.CancelableFuture
 import monix.reactive.MulticastStrategy
 import monix.reactive.Observable
+
 object CompileTask {
   private implicit val logContext: DebugFilter = DebugFilter.Compilation
   def compile[UseSiteLogger <: Logger](
@@ -98,7 +98,6 @@ object CompileTask {
           compileProjectTracer.terminate()
           Task.now(earlyResultBundle)
         case Right(CompileSourcesAndInstance(sources, instance, _)) =>
-          val externalUserClassesDir = bundle.clientClassesDir
           val readOnlyClassesDir = lastSuccessful.classesDir
           val newClassesDir = compileOut.internalNewClassesDir
           val classpath = bundle.dependenciesData.buildFullCompileClasspathFor(
@@ -173,7 +172,7 @@ object CompileTask {
                 val postCompilationTasks =
                   backgroundTasks
                     .trigger(
-                      externalUserClassesDir,
+                      bundle.clientClassesObserver,
                       reporter.underlying,
                       compileProjectTracer,
                       logger
@@ -247,14 +246,14 @@ object CompileTask {
       val o = state.commonOptions
       val cancel = cancelCompilation
       val logger = ObservedLogger(rawLogger, observer)
-      val dir = state.client.getUniqueClassesDirFor(inputs.project, forceGeneration = true)
+      val clientClassesObserver = state.client.getClassesObserverFor(inputs.project)
       val underlying = createReporter(ReporterInputs(inputs.project, cwd, rawLogger))
       val reporter = new ObservedReporter(logger, underlying)
       val sourceGeneratorCache = state.sourceGeneratorCache
       CompileBundle.computeFrom(
         inputs,
         sourceGeneratorCache,
-        dir,
+        clientClassesObserver,
         reporter,
         last,
         prev,
@@ -292,7 +291,7 @@ object CompileTask {
           } else {
             results.foreach {
               case FinalNormalCompileResult.HasException(project, err) =>
-                val errMsg = err.fold(identity, BloopLogger.prettyPrintException)
+                val errMsg = err.fold(identity, Logger.prettyPrintException)
                 rawLogger.error(s"Unexpected error when compiling ${project.name}: $errMsg")
               case _ => () // Do nothing when the final compilation result is not an actual error
             }
diff --git a/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala b/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala
index c6b379dcf6..4fe6c4ba3b 100644
--- a/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala
+++ b/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala
@@ -19,7 +19,7 @@ object LinkTask {
       project: Project,
       state: State,
       mainClass: String,
-      target: AbsolutePath,
+      targetDirectory: AbsolutePath,
       platform: Platform.Js
   ): Task[State] = {
     import state.logger
@@ -36,10 +36,19 @@ object LinkTask {
             // Pass in the default scheduler used by this task to the linker
             Task.deferAction { s =>
               toolchain
-                .link(config, project, fullClasspath, true, Some(mainClass), target, s, logger)
+                .link(
+                  config,
+                  project,
+                  fullClasspath,
+                  true,
+                  Some(mainClass),
+                  targetDirectory,
+                  s,
+                  logger
+                )
                 .map {
                   case scala.util.Success(_) =>
-                    state.withInfo(s"Generated JavaScript file '${target.syntax}'")
+                    state.withInfo(s"Generated JavaScript file '${targetDirectory.syntax}'")
                   case scala.util.Failure(t) =>
                     val msg = Feedback.failedToLink(project, ScalaJsToolchain.name, t)
                     state.withError(msg, ExitStatus.LinkingError).withTrace(t)
diff --git a/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala b/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala
index b1df2daf6a..72a181fc77 100644
--- a/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala
+++ b/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala
@@ -73,7 +73,7 @@ object Tasks {
         val loader = ClasspathUtil.makeLoader(pathEntries, instance)
         val compiler =
           state.compilerCache
-            .get(instance, javacBin, project.javacOptions)
+            .get(instance, project.classpathOptions, javacBin, project.javacOptions)
             .scalac
             .asInstanceOf[AnalyzingCompiler]
         val options = project.scalacOptions :+ "-Xnojline"
diff --git a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala
index f741483000..89e695c540 100644
--- a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala
+++ b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala
@@ -23,6 +23,7 @@ import bloop.tracing.BraveTracer
 
 import monix.reactive.Observable
 import sbt.internal.inc.PlainVirtualFileConverter
+import bloop.ClientClassesObserver
 
 sealed trait CompileBundle
 
@@ -75,7 +76,7 @@ case object CancelledCompileBundle extends CompileBundle
  */
 final case class SuccessfulCompileBundle(
     project: Project,
-    clientClassesDir: AbsolutePath,
+    clientClassesObserver: ClientClassesObserver,
     dependenciesData: CompileDependenciesData,
     javaSources: List[AbsolutePath],
     scalaSources: List[AbsolutePath],
@@ -95,7 +96,7 @@ final case class SuccessfulCompileBundle(
       project.out,
       project.analysisOut,
       project.genericClassesDir,
-      clientClassesDir,
+      clientClassesObserver.classesDir,
       readOnlyClassesDir
     )
   }
@@ -152,7 +153,7 @@ object CompileBundle {
   def computeFrom(
       inputs: CompileDefinitions.BundleInputs,
       sourceGeneratorCache: SourceGeneratorCache,
-      clientExternalClassesDir: AbsolutePath,
+      clientClassesObserver: ClientClassesObserver,
       reporter: ObservedReporter,
       lastSuccessful: LastSuccessfulResult,
       lastResult: Compiler.Result,
@@ -228,7 +229,7 @@ object CompileBundle {
 
           new SuccessfulCompileBundle(
             project,
-            clientExternalClassesDir,
+            clientClassesObserver,
             compileDependenciesData,
             javaSources,
             scalaSources,
diff --git a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala
index 1ad3840fda..f596ce335f 100644
--- a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala
+++ b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala
@@ -166,8 +166,7 @@ object CompileGraph {
         val previousProblems =
           Compiler.previousProblemsFromResult(bundle.latestResult, previousSuccessfulProblems)
 
-        val externalClassesDir =
-          client.getUniqueClassesDirFor(bundle.project, forceGeneration = true)
+        val clientClassesObserver = client.getClassesObserverFor(bundle.project)
 
         // Replay events asynchronously to waiting for the compilation result
         import scala.concurrent.duration.FiniteDuration
@@ -210,7 +209,7 @@ object CompileGraph {
                       reporter.processEndCompilation(
                         previousSuccessfulProblems,
                         a.code,
-                        Some(externalClassesDir),
+                        Some(clientClassesObserver.classesDir),
                         Some(bundle.out.analysisOut)
                       )
                   }
@@ -263,7 +262,7 @@ object CompileGraph {
                   case s: Compiler.Result.Success =>
                     // Wait on new classes to be populated for correctness
                     val runningBackgroundTasks = s.backgroundTasks
-                      .trigger(externalClassesDir, reporter, bundle.tracer, logger)
+                      .trigger(clientClassesObserver, reporter, bundle.tracer, logger)
                       .runAsync(ExecutionContext.ioScheduler)
                     Task.now(results.copy(runningBackgroundTasks = runningBackgroundTasks))
                   case _: Compiler.Result.Cancelled =>
diff --git a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala
index 3e95405bd6..d6191d7036 100644
--- a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala
+++ b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala
@@ -119,8 +119,8 @@ object ScalaJsToolchain extends ToolchainCompanion[ScalaJsToolchain] {
 
   def linkTargetFrom(project: Project, config: JsConfig): AbsolutePath = {
     config.output match {
-      case Some(p) => AbsolutePath(p)
-      case None => project.out.resolve(s"${project.name}.js")
+      case Some(p) if p.toFile().isDirectory() => AbsolutePath(p)
+      case None => project.out.resolve(s"${project.name}-js").createDirectories
     }
   }
 
diff --git a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala
index aa5faeb6e2..1b51af487c 100644
--- a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala
+++ b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala
@@ -9,10 +9,12 @@ import bloop.DependencyResolution
 import bloop.config.Config
 import bloop.config.Config.NativeConfig
 import bloop.data.Project
+import bloop.engine.ExecutionContext
 import bloop.internal.build.BuildInfo
 import bloop.io.AbsolutePath
 import bloop.logging.Logger
 import bloop.task.Task
+import scala.concurrent.Future
 
 final class ScalaNativeToolchain private (classLoader: ClassLoader) {
 
@@ -35,7 +37,10 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) {
       logger: Logger
   ): Task[Try[Unit]] = {
     val bridgeClazz = classLoader.loadClass("bloop.scalanative.NativeBridge")
-    val nativeLinkMeth = bridgeClazz.getMethod("nativeLink", paramTypes: _*)
+    val isNative05 = config.version.startsWith("0.5")
+    val nativeLinkMeth =
+      if (isNative05) bridgeClazz.getMethod("nativeLink", paramTypes05: _*)
+      else bridgeClazz.getMethod("nativeLink", paramTypes04: _*)
 
     // Scala Native 0.4.{0,1,2} expect to receive the companion object class' name
     val fullEntry = config.version match {
@@ -44,15 +49,32 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) {
       case _ =>
         mainClass.stripSuffix("$")
     }
-    val linkage = Task {
-      nativeLinkMeth
-        .invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger)
-        .asInstanceOf[Unit]
-    }.materialize
-
+    val linkage = if (isNative05) {
+      Task.fromFuture {
+        nativeLinkMeth
+          .invoke(
+            null,
+            config,
+            project,
+            fullClasspath,
+            fullEntry,
+            target.underlying,
+            logger,
+            ExecutionContext.ioScheduler
+          )
+          .asInstanceOf[Future[Unit]]
+      }.materialize
+    } else {
+      Task {
+        nativeLinkMeth
+          .invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger)
+          .asInstanceOf[Unit]
+      }.materialize
+    }
     linkage.map {
       case s @ scala.util.Success(_) => s
       case f @ scala.util.Failure(t) =>
+        t.printStackTrace()
         t match {
           case it: InvocationTargetException => scala.util.Failure(it.getCause)
           case _ => f
@@ -60,9 +82,12 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) {
     }
   }
 
-  // format: OFF
-  private val paramTypes = classOf[NativeConfig] :: classOf[Project] :: classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil
-  // format: ON
+  private val paramTypes04 = classOf[NativeConfig] :: classOf[Project] ::
+    classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil
+
+  private val paramTypes05 = classOf[NativeConfig] :: classOf[Project] ::
+    classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] ::
+    classOf[scala.concurrent.ExecutionContext] :: Nil
 }
 
 object ScalaNativeToolchain extends ToolchainCompanion[ScalaNativeToolchain] {
@@ -73,7 +98,8 @@ object ScalaNativeToolchain extends ToolchainCompanion[ScalaNativeToolchain] {
   override def artifactNameFrom(version: String): String = {
     if (version.length == 3) sys.error("The full Scala Native version must be provided")
     else if (version.startsWith("0.4")) BuildInfo.nativeBridge04
-    else sys.error(s"Expected compatible Scala Native version [0.3, 0.4], $version given")
+    else if (version.startsWith("0.5")) BuildInfo.nativeBridge05
+    else sys.error(s"Expected compatible Scala Native version [0.4, 0.5], $version given")
   }
 
   override def getPlatformData(platform: Platform): Option[PlatformData] = {
diff --git a/frontend/src/main/scala/bloop/io/SourceWatcher.scala b/frontend/src/main/scala/bloop/io/SourceWatcher.scala
index 3838376d42..eeb46094d8 100644
--- a/frontend/src/main/scala/bloop/io/SourceWatcher.scala
+++ b/frontend/src/main/scala/bloop/io/SourceWatcher.scala
@@ -42,7 +42,7 @@ final class SourceWatcher private (
     def runAction(state: State, events: Seq[DirectoryChangeEvent]): Task[State] = {
       // Windows is not supported for now
       if (!bloop.util.CrossPlatform.isWindows)
-        logger.info("\u001b[H\u001b[2J") // Clean terminal before acting on the event action
+        logger.info(bloop.util.Console.clearCommand)
       events.foreach(e => logger.debug(s"A ${e.eventType()} in ${e.path()} has triggered an event"))
       action(state)
     }
diff --git a/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala b/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala
index 60fd679297..52544ae32d 100644
--- a/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala
+++ b/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala
@@ -295,13 +295,7 @@ final class BspProjectReporter(
   ): Unit = {
     val problemsInPreviousAnalysisPerFile = Reporter.groupProblemsByFile(previousSuccessfulProblems)
 
-    def mockNoOpCompileEventsAndEnd: CompilationEvent.EndCompilation = {
-      // When no-op, we keep reporting the start and the end of compilation for consistency
-      val startMsg = s"Start no-op compilation for ${project.name}"
-      logger.publishCompilationStart(
-        CompilationEvent.StartCompilation(project.name, project.bspUri, startMsg, taskId)
-      )
-
+    def recheckProblems = {
       recentlyReportProblemsPerFile.foreach {
         case (sourceFile, problemsPerFile) if reportAllPreviousProblems =>
           reportAllProblems(sourceFile, problemsPerFile)
@@ -326,30 +320,17 @@ final class BspProjectReporter(
               logger.noDiagnostic(CompilationEvent.NoDiagnostic(project.bspUri, sourceFile))
           }
       }
-
-      val liftedProblems = allProblems.toIterator.map(super.liftFatalWarning(_)).toList
-      CompilationEvent.EndCompilation(
-        project.name,
-        project.bspUri,
-        taskId,
-        liftedProblems,
-        code,
-        isNoOp = true,
-        isLastCycle = true,
-        clientClassesDir,
-        clientAnalysisOut
-      )
     }
 
-    endEvent = Some(
-      if (cycleCount.get == 0) mockNoOpCompileEventsAndEnd
-      else {
-        // Great, let's report the pending end incremental cycle as the last one
-        val inputs =
-          CycleInputs(true, problemsInPreviousAnalysisPerFile, clientClassesDir, clientAnalysisOut)
-        processEndPreviousCycle(inputs, Some(code))
-      }
-    )
+    endEvent = if (cycleCount.get == 0) {
+      recheckProblems
+      None
+    } else {
+      // Great, let's report the pending end incremental cycle as the last one
+      val inputs =
+        CycleInputs(true, problemsInPreviousAnalysisPerFile, clientClassesDir, clientAnalysisOut)
+      Some(processEndPreviousCycle(inputs, Some(code)))
+    }
 
     // Clear the state of files with problems at the end of compilation
     clearedFilesForClient.clear()
diff --git a/frontend/src/main/scala/bloop/testing/TestInternals.scala b/frontend/src/main/scala/bloop/testing/TestInternals.scala
index 9637a519eb..8d6b26a20b 100644
--- a/frontend/src/main/scala/bloop/testing/TestInternals.scala
+++ b/frontend/src/main/scala/bloop/testing/TestInternals.scala
@@ -340,7 +340,7 @@ object TestInternals {
     } catch {
       case _: ClassNotFoundException => None
       case NonFatal(t) =>
-        logger.report(s"Initialisation of test framework $fqn failed", t)
+        logger.error(s"Initialisation of test framework $fqn failed", t)
         None
     }
   }
diff --git a/frontend/src/main/scala/bloop/util/Console.scala b/frontend/src/main/scala/bloop/util/Console.scala
new file mode 100644
index 0000000000..55438fd1ea
--- /dev/null
+++ b/frontend/src/main/scala/bloop/util/Console.scala
@@ -0,0 +1,10 @@
+package bloop.util
+
+object Console {
+  private val escape = "\u001b["
+  private val cursorHome = "H"
+  private val eraseScreen = "2J"
+  private val eraseScrollbar = "3J"
+
+  val clearCommand: String = escape + cursorHome + escape + eraseScreen + escape + eraseScrollbar
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt
index d6e44edf90..491b1868f7 100644
--- a/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt
@@ -6,7 +6,7 @@ lazy val `test-project` =
     .withoutSuffixFor(JVMPlatform)
     .settings(
       name := "test-project",
-      scalaVersion := "2.13.4",
+      scalaVersion := "2.13.13",
       mainClass in (Compile, run) := Some("hello.App")
     )
 
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt
index c3d5191808..f674586ddd 100644
--- a/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt
@@ -1,5 +1,5 @@
 addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0")
-addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.3")
+addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
 
 val pluginVersion = sys.props.getOrElse(
   "bloopVersion",
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/build.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.5/build.sbt
new file mode 100644
index 0000000000..491b1868f7
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/build.sbt
@@ -0,0 +1,20 @@
+bloopExportJarClassifiers in Global := Some(Set("sources"))
+bloopConfigDir in Global := baseDirectory.value / "bloop-config"
+
+lazy val `test-project` =
+  crossProject(NativePlatform, JVMPlatform)
+    .withoutSuffixFor(JVMPlatform)
+    .settings(
+      name := "test-project",
+      scalaVersion := "2.13.13",
+      mainClass in (Compile, run) := Some("hello.App")
+    )
+
+lazy val `test-project-native` = `test-project`.native.settings(
+  // Should override default set above. Tested as part of ScalaNativeToolchainSpec.
+  (Compile / run / bloopMainClass) := Some("hello.DefaultApp")
+)
+
+lazy val `test-project-jvm` = `test-project`.jvm.settings(
+  (Compile / run / bloopMainClass) := Some("hello.App")
+)
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/build.properties b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/build.properties
new file mode 100644
index 0000000000..b089b60c7a
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/build.properties
@@ -0,0 +1,2 @@
+sbt.version=1.9.9
+
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/plugins.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/plugins.sbt
new file mode 100644
index 0000000000..cf1c2ba4a5
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/plugins.sbt
@@ -0,0 +1,11 @@
+addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0")
+addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.0-RC2")
+
+val pluginVersion = sys.props.getOrElse(
+  "bloopVersion",
+  throw new RuntimeException("Unable to find -DbloopVersion")
+)
+
+addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % pluginVersion)
+
+updateOptions := updateOptions.value.withLatestSnapshots(false)
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/native/src/main/scala/hello/DefaultApp.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/native/src/main/scala/hello/DefaultApp.scala
new file mode 100644
index 0000000000..f29441fdf8
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/native/src/main/scala/hello/DefaultApp.scala
@@ -0,0 +1,10 @@
+package hello
+
+import scalanative.unsafe._
+import scalanative.libc.stdio
+
+object DefaultApp {
+  def main(args: Array[String]): Unit = Zone { implicit z =>
+    stdio.vprintf(c"Hello, world from DefaultApp!\n", toCVarArgList())
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/App.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/App.scala
new file mode 100644
index 0000000000..344dc7e549
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/App.scala
@@ -0,0 +1,7 @@
+package hello
+
+object App {
+  def main(args: Array[String]): Unit = {
+    println(Hello.greet("world"))
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Environment.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Environment.scala
new file mode 100644
index 0000000000..9d92c70ec3
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Environment.scala
@@ -0,0 +1,7 @@
+package hello
+
+object Environment {
+  def requireEnvironmentVariable(): Unit = {
+    sys.env.get("BLOOP_OWNER").getOrElse(sys.error("Missing BLOOP_OWNER env variable!"))
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Hello.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Hello.scala
new file mode 100644
index 0000000000..45671be900
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Hello.scala
@@ -0,0 +1,9 @@
+package hello
+
+object Hello {
+  def greet(name: String): String = {
+    // We do this to check that environment variables work
+    Environment.requireEnvironmentVariable()
+    s"Hello, $name!"
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/EternalUTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/EternalUTest.scala
new file mode 100644
index 0000000000..e60441b7fc
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/EternalUTest.scala
@@ -0,0 +1,11 @@
+package hello
+
+import utest._
+
+object EternalUTest extends TestSuite {
+  val tests = Tests {
+    "This test never ends" - {
+      while (true) ()
+    }
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/JUnitTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/JUnitTest.scala
new file mode 100644
index 0000000000..6c3325dae9
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/JUnitTest.scala
@@ -0,0 +1,11 @@
+package hello
+
+import org.junit.Test
+import org.junit.Assert.assertEquals
+
+class JUnitTest {
+  @Test
+  def myTest: Unit = {
+    assertEquals(4, 2 + 2)
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaCheckTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaCheckTest.scala
new file mode 100644
index 0000000000..0a52e66501
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaCheckTest.scala
@@ -0,0 +1,11 @@
+package hello
+
+import org.scalacheck.Properties
+import org.scalacheck.Prop.forAll
+
+object ScalaCheckTest extends Properties("Greeting") {
+
+  property("is personal") = forAll { (name: String) =>
+    Hello.greet(name) == s"Hello, $name!"
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaTestTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaTestTest.scala
new file mode 100644
index 0000000000..c9b6a92ea1
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaTestTest.scala
@@ -0,0 +1,9 @@
+package hello
+
+import org.scalatest._
+
+class ScalaTestTest extends FlatSpec with Matchers {
+  "A greeting" should "be very personal" in {
+    Hello.greet("Martin") shouldBe "Hello, Martin!"
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/Specs2Test.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/Specs2Test.scala
new file mode 100644
index 0000000000..373220d148
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/Specs2Test.scala
@@ -0,0 +1,17 @@
+package hello
+
+import org.specs2._
+
+class Specs2Test extends Specification {
+  def is = s2"""
+
+  This is a specification to check the `Hello` object.
+
+  A greeting
+    is very personal              $isPersonal
+  """
+
+  def isPersonal = {
+    Hello.greet("Martin") must beEqualTo("Hello, Martin!")
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/UTestTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/UTestTest.scala
new file mode 100644
index 0000000000..6022e8847b
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/UTestTest.scala
@@ -0,0 +1,15 @@
+package hello
+
+import utest._
+
+object UTestTest extends TestSuite {
+  import utest.framework.Formatter
+  override def utestFormatter: Formatter = new Formatter {
+    override def formatColor = false
+  }
+  val tests = Tests {
+    "Greetings are very personal" - {
+      assert(Hello.greet("Martin") == "Hello, Martin!")
+    }
+  }
+}
diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/WritingTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/WritingTest.scala
new file mode 100644
index 0000000000..4868ea6968
--- /dev/null
+++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/WritingTest.scala
@@ -0,0 +1,9 @@
+package hello
+
+import org.scalatest._
+
+class WritingTest extends FlatSpec with Matchers {
+  "A test" should "be able to print stuff" in {
+    (1 to 10).foreach(_ => println("message"))
+  }
+}
diff --git a/frontend/src/test/resources/scala-java-processorpath/build.sbt b/frontend/src/test/resources/scala-java-processorpath/build.sbt
index 6e19092d72..6461a454a1 100644
--- a/frontend/src/test/resources/scala-java-processorpath/build.sbt
+++ b/frontend/src/test/resources/scala-java-processorpath/build.sbt
@@ -1,6 +1,6 @@
 bloopConfigDir in Global := baseDirectory.value / "bloop-config"
 
-ThisBuild / scalaVersion := "2.12.18"
+ThisBuild / scalaVersion := "2.12.19"
 ThisBuild / version := "0.1.0-SNAPSHOT"
 ThisBuild / organization := "com.example"
 ThisBuild / organizationName := "example"
diff --git a/frontend/src/test/scala/bloop/BuildLoaderSpec.scala b/frontend/src/test/scala/bloop/BuildLoaderSpec.scala
index 14528e906b..b8b0f68ddb 100644
--- a/frontend/src/test/scala/bloop/BuildLoaderSpec.scala
+++ b/frontend/src/test/scala/bloop/BuildLoaderSpec.scala
@@ -9,6 +9,7 @@ import bloop.data.TraceSettings
 import bloop.data.WorkspaceSettings
 import bloop.engine.Build
 import bloop.internal.build.BuildInfo
+import bloop.internal.build.BuildTestInfo
 import bloop.io.AbsolutePath
 import bloop.logging.RecordingLogger
 import bloop.task.Task
@@ -17,7 +18,7 @@ import bloop.tracing.TraceProperties
 import bloop.util.TestUtil
 
 object BuildLoaderSpec extends BaseSuite {
-  val semanticdbVersion = "4.8.4"
+  val semanticdbVersion = "4.8.15"
   val oldSemanticdbVersion = "4.5.13"
 
   testLoad("don't reload if nothing changes") { (testBuild, logger) =>
diff --git a/frontend/src/test/scala/bloop/DeduplicationSpec.scala b/frontend/src/test/scala/bloop/DeduplicationSpec.scala
index bed80eca33..ff76849202 100644
--- a/frontend/src/test/scala/bloop/DeduplicationSpec.scala
+++ b/frontend/src/test/scala/bloop/DeduplicationSpec.scala
@@ -109,10 +109,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
 
         assertNoDiff(
           firstCompiledState.lastDiagnostics(build.userProject),
-          """#1: task start 2
+          """#1: task start 1
             |  -> Msg: Compiling user (2 Scala sources)
             |  -> Data kind: compile-task
-            |#1: task finish 2
+            |#1: task finish 1
             |  -> errors 0, warnings 0
             |  -> Msg: Compiled 'user'
             |  -> Data kind: compile-report
@@ -151,14 +151,7 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
 
         assertNoDiff(
           firstCompiledState.lastDiagnostics(build.userProject),
-          """#2: task start 4
-            |  -> Msg: Start no-op compilation for user
-            |  -> Data kind: compile-task
-            |#2: task finish 4
-            |  -> errors 0, warnings 0
-            |  -> Msg: Compiled 'user'
-            |  -> Data kind: compile-report
-        """.stripMargin
+          "" // expect None here since it's a no-op which turns into ""
         )
 
         // Same check as before because no-op should not show any more input
@@ -242,10 +235,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
 
         assertNoDiff(
           firstCompiledState.lastDiagnostics(build.userProject),
-          """#1: task start 2
+          """#1: task start 1
             |  -> Msg: Compiling user (2 Scala sources)
             |  -> Data kind: compile-task
-            |#1: task finish 2
+            |#1: task finish 1
             |  -> errors 0, warnings 0
             |  -> Msg: Compiled 'user'
             |  -> Data kind: compile-report
@@ -335,10 +328,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
 
         assertNoDiff(
           secondCompiledState.lastDiagnostics(build.userProject),
-          """#2: task start 4
+          """#2: task start 2
             |  -> Msg: Compiling user (1 Scala source)
             |  -> Data kind: compile-task
-            |#2: task finish 4
+            |#2: task finish 2
             |  -> errors 0, warnings 0
             |  -> Msg: Compiled 'user'
             |  -> Data kind: compile-report
@@ -426,10 +419,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
         // First compilation should not report warning
         assertNoDiff(
           firstCompiledState.lastDiagnostics(build.userProject),
-          """#1: task start 2
+          """#1: task start 1
             |  -> Msg: Compiling user (2 Scala sources)
             |  -> Data kind: compile-task
-            |#1: task finish 2
+            |#1: task finish 1
             |  -> errors 0, warnings 0
             |  -> Msg: Compiled 'user'
             |  -> Data kind: compile-report
@@ -585,13 +578,13 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
         // We reproduce the same streaming side effects during compilation
         assertNoDiff(
           firstCompiledState.lastDiagnostics(`B`),
-          """#1: task start 2
+          """#1: task start 1
             |  -> Msg: Compiling b (1 Scala source)
             |  -> Data kind: compile-task
             |#1: b/src/B.scala
             |  -> List(Diagnostic(Range(Position(2,28),Position(2,28)),Some(Error),Some(_),Some(_),type mismatch;  found   : Int  required: String,None,None,Some({"actions":[]})))
             |  -> reset = true
-            |#1: task finish 2
+            |#1: task finish 1
             |  -> errors 1, warnings 0
             |  -> Msg: Compiled 'b'
             |  -> Data kind: compile-report
@@ -666,13 +659,13 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
 
         assertNoDiff(
           secondBspState.lastDiagnostics(`B`),
-          """#2: task start 4
+          """#2: task start 2
             |  -> Msg: Compiling b (1 Scala source)
             |  -> Data kind: compile-task
             |#2: b/src/B.scala
             |  -> List(Diagnostic(Range(Position(2,28),Position(2,28)),Some(Error),Some(_),Some(_),type mismatch;  found   : Int  required: String,None,None,Some({"actions":[]})))
             |  -> reset = true
-            |#2: task finish 4
+            |#2: task finish 2
             |  -> errors 1, warnings 0
             |  -> Msg: Compiled 'b'
             |  -> Data kind: compile-report
@@ -766,13 +759,13 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
 
         assertNoDiff(
           thirdBspState.lastDiagnostics(`B`),
-          """#3: task start 6
+          """#3: task start 3
             |  -> Msg: Compiling b (1 Scala source)
             |  -> Data kind: compile-task
             |#3: b/src/B.scala
             |  -> List()
             |  -> reset = true
-            |#3: task finish 6
+            |#3: task finish 3
             |  -> errors 0, warnings 0
             |  -> Msg: Compiled 'b'
             |  -> Data kind: compile-report
@@ -923,10 +916,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite {
           assertNoDiff(
             firstCompiledState.lastDiagnostics(`B`),
             s"""
-               |#1: task start 2
+               |#1: task start 1
                |  -> Msg: Compiling b (6 Scala sources)
                |  -> Data kind: compile-task
-               |#1: task finish 2
+               |#1: task finish 1
                |  -> errors 0, warnings 0
                |  -> Msg: Compiled 'b'
                |  -> Data kind: compile-report
diff --git a/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala b/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala
index 0afdc2b209..99023195a7 100644
--- a/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala
+++ b/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala
@@ -77,7 +77,7 @@ object ScalaVersionsSpec extends bloop.testing.BaseSuite {
     "2.13.10",
     "3.1.3",
     "3.2.1",
-    "2.13.12-bin-86f40c2"
+    "2.13.13"
   )
 
   val allVersions = scalaVersions
diff --git a/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala b/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala
index 6cc6ce0f57..544f662e50 100644
--- a/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala
+++ b/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala
@@ -349,6 +349,22 @@ abstract class BspBaseSuite extends BaseSuite with BspClientTest {
       TestUtil.await(FiniteDuration(5, "s"))(resourcesTask)
     }
 
+    def requestDependencyModules(project: TestProject): bsp.DependencyModulesResult = {
+      val dependencyModulesTask = {
+        client0
+          .request(
+            endpoints.BuildTarget.dependencyModules,
+            bsp.DependencyModulesParams(List(project.bspId))
+          )
+          .map {
+            case RpcFailure(_, error) => fail(s"Received error ${error}")
+            case RpcSuccess(modules, _) => modules
+          }
+      }
+
+      TestUtil.await(FiniteDuration(5, "s"))(dependencyModulesTask)
+    }
+
     def requestDependencySources(project: TestProject): bsp.DependencySourcesResult = {
       val dependencySourcesTask = {
         client0
diff --git a/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala b/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala
index 4766a785f2..31036278fc 100644
--- a/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala
+++ b/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala
@@ -86,13 +86,7 @@ class BspCompileSpec(
         assertSameExternalClassesDirs(compiledState, secondCompiledState, projects)
         assertNoDiff(
           secondCompiledState.lastDiagnostics(`A`),
-          """#2: task start 2
-            |  -> Msg: Start no-op compilation for a
-            |  -> Data kind: compile-task
-            |#2: task finish 2
-            |  -> errors 0, warnings 0
-            |  -> Msg: Compiled 'a'
-            |  -> Data kind: compile-report""".stripMargin
+          "" // no-op
         )
       }
     }
@@ -586,17 +580,10 @@ class BspCompileSpec(
         assertSameExternalClassesDirs(thirdCompiledState, compiledState, projects)
         assertNoDiff(
           thirdCompiledState.lastDiagnostics(`A`),
-          """#3: task start 3
-            |  -> Msg: Start no-op compilation for a
-            |  -> Data kind: compile-task
-            |#3: a/src/A.scala
+          """#3: a/src/A.scala
             |  -> List()
             |  -> reset = true
-            |#3: task finish 3
-            |  -> errors 0, warnings 0
-            |  -> Msg: Compiled 'a'
-            |  -> Data kind: compile-report
-            """.stripMargin
+            """.stripMargin // no-op so it only gives the compile-report
         )
 
         writeFile(`A`.srcFor("/A.scala"), Sources.`A3.scala`)
@@ -613,7 +600,7 @@ class BspCompileSpec(
         assertSameExternalClassesDirs(fourthCompiledState, compiledState, projects)
         assertNoDiff(
           fourthCompiledState.lastDiagnostics(`A`),
-          """#4: task start 4
+          """#4: task start 3
             |  -> Msg: Compiling a (1 Scala source)
             |  -> Data kind: compile-task
             |#4: a/src/A.scala
@@ -622,7 +609,7 @@ class BspCompileSpec(
             |#4: a/src/A.scala
             |  -> List(Diagnostic(Range(Position(0,0),Position(0,26)),Some(Warning),Some(_),Some(_),Unused import,None,None,Some({"actions":[]})))
             |  -> reset = false
-            |#4: task finish 4
+            |#4: task finish 3
             |  -> errors 1, warnings 1
             |  -> Msg: Compiled 'a'
             |  -> Data kind: compile-report
@@ -636,13 +623,13 @@ class BspCompileSpec(
         assertDifferentExternalClassesDirs(fifthCompiledState, compiledState, projects)
         assertNoDiff(
           fifthCompiledState.lastDiagnostics(`A`),
-          """#5: task start 5
+          """#5: task start 4
             |  -> Msg: Compiling a (1 Scala source)
             |  -> Data kind: compile-task
             |#5: a/src/A.scala
             |  -> List(Diagnostic(Range(Position(0,0),Position(0,26)),Some(Warning),Some(_),Some(_),Unused import,None,None,Some({"actions":[]})))
             |  -> reset = true
-            |#5: task finish 5
+            |#5: task finish 4
             |  -> errors 0, warnings 1
             |  -> Msg: Compiled 'a'
             |  -> Data kind: compile-report
@@ -662,17 +649,17 @@ class BspCompileSpec(
         assertSameExternalClassesDirs(sixthCompiledState, fifthCompiledState, projects)
         assertNoDiff(
           sixthCompiledState.lastDiagnostics(`A`),
-          """#6: task start 6
+          """#6: task start 5
             |  -> Msg: Compiling a (1 Scala source)
             |  -> Data kind: compile-task
             |#6: a/src/A.scala
             |  -> List()
             |  -> reset = true
-            |#6: task finish 6
+            |#6: task finish 5
             |  -> errors 0, warnings 0
             |  -> Msg: Compiled 'a'
             |  -> Data kind: compile-report
-            |#6: task start 6
+            |#6: task start 5
             |  -> Msg: Compiling a (1 Scala source)
             |  -> Data kind: compile-task
             |#6: a/src/A.scala
@@ -681,7 +668,7 @@ class BspCompileSpec(
             |#6: a/src/A.scala
             |  -> List(Diagnostic(Range(Position(1,0),Position(3,1)),Some(Error),Some(_),Some(_),object creation impossible, since value y in trait Base of type Int is not defined,None,None,Some({"actions":[]})))
             |  -> reset = false
-            |#6: task finish 6
+            |#6: task finish 5
             |  -> errors 1, warnings 1
             |  -> Msg: Compiled 'a'
             |  -> Data kind: compile-report
@@ -696,13 +683,13 @@ class BspCompileSpec(
 
         assertNoDiff(
           seventhCompiledState.lastDiagnostics(`A`),
-          """#7: task start 7
+          """#7: task start 6
             |  -> Msg: Compiling a (1 Scala source)
             |  -> Data kind: compile-task
             |#7: a/src/A.scala
             |  -> List(Diagnostic(Range(Position(0,0),Position(0,26)),Some(Warning),Some(_),Some(_),Unused import,None,None,Some({"actions":[]})))
             |  -> reset = true
-            |#7: task finish 7
+            |#7: task finish 6
             |  -> errors 0, warnings 0
             |  -> Msg: Compiled 'a'
             |  -> Data kind: compile-report
@@ -753,18 +740,12 @@ class BspCompileSpec(
         assertSameExternalClassesDirs(compiledState.toTestState, secondCliCompiledState, `A`)
 
         // BSP publishes warnings even if it's a no-op
+        // however it skips the task start/end
         assertNoDiff(
           compiledState.lastDiagnostics(`A`),
-          """#1: task start 1
-            |  -> Msg: Start no-op compilation for a
-            |  -> Data kind: compile-task
-            |#1: a/src/main/scala/App.scala
+          """#1: a/src/main/scala/App.scala
             |  -> List(Diagnostic(Range(Position(2,4),Position(2,4)),Some(Warning),Some(_),Some(_),a pure expression does nothing in statement position,None,None,Some({"actions":[]})))
-            |  -> reset = true
-            |#1: task finish 1
-            |  -> errors 0, warnings 0
-            |  -> Msg: Compiled 'a'
-            |  -> Data kind: compile-report""".stripMargin
+            |  -> reset = true""".stripMargin
         )
       }
     }
@@ -987,16 +968,9 @@ class BspCompileSpec(
 
         assertNoDiff(
           thirdCompiledState.lastDiagnostics(`A`),
-          """#3: task start 3
-            |  -> Msg: Start no-op compilation for a
-            |  -> Data kind: compile-task
-            |#3: a/src/main/scala/Bar.scala
+          """#3: a/src/main/scala/Bar.scala
             |  -> List()
-            |  -> reset = true
-            |#3: task finish 3
-            |  -> errors 0, warnings 0
-            |  -> Msg: Compiled 'a'
-            |  -> Data kind: compile-report""".stripMargin
+            |  -> reset = true""".stripMargin
         )
       }
     }
@@ -1098,14 +1072,7 @@ class BspCompileSpec(
 
         assertNoDiff(
           compiledState.lastDiagnostics(`B`),
-          """|#2: task start 4
-             |  -> Msg: Start no-op compilation for b
-             |  -> Data kind: compile-task
-             |#2: task finish 4
-             |  -> errors 0, warnings 0
-             |  -> Msg: Compiled 'b'
-             |  -> Data kind: compile-report
-             |""".stripMargin
+          "" // no-op
         )
 
         writeFile(`A`.srcFor("/Foo.scala"), Sources.`Foo3.scala`)
@@ -1115,7 +1082,7 @@ class BspCompileSpec(
         assertValidCompilationState(thirdCompiledState, projects)
         assertNoDiff(
           thirdCompiledState.lastDiagnostics(`A`),
-          """|#3: task start 5
+          """|#3: task start 4
              |  -> Msg: Compiling a (1 Scala source)
              |  -> Data kind: compile-task
              |#3: a/src/Foo.scala
@@ -1124,7 +1091,7 @@ class BspCompileSpec(
              |#3: a/src/Foo.scala
              |  -> List(Diagnostic(Range(Position(1,0),Position(1,7)),Some(Error),Some(_),Some(_),Unused import,None,None,Some({"actions":[]})))
              |  -> reset = false
-             |#3: task finish 5
+             |#3: task finish 4
              |  -> errors 2, warnings 0
              |  -> Msg: Compiled 'a'
              |  -> Data kind: compile-report
@@ -1133,13 +1100,13 @@ class BspCompileSpec(
 
         assertNoDiff(
           compiledState.lastDiagnostics(`B`),
-          """|#3: task start 6
+          """|#3: task start 5
              |  -> Msg: Compiling b (1 Scala source)
              |  -> Data kind: compile-task
              |#3: b/src/Buzz.scala
              |  -> List(Diagnostic(Range(Position(0,0),Position(0,7)),Some(Error),Some(_),Some(_),Unused import,None,None,Some({"actions":[]})))
              |  -> reset = true
-             |#3: task finish 6
+             |#3: task finish 5
              |  -> errors 1, warnings 0
              |  -> Msg: Compiled 'b'
              |  -> Data kind: compile-report
diff --git a/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala b/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala
index d1814c121e..97d57eed5c 100644
--- a/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala
+++ b/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala
@@ -11,6 +11,7 @@ import bloop.cli.ExitStatus
 import bloop.data.WorkspaceSettings
 import bloop.engine.ExecutionContext
 import bloop.internal.build.BuildInfo
+import bloop.internal.build.BuildTestInfo
 import bloop.io.AbsolutePath
 import bloop.io.Environment.LineSplitter
 import bloop.io.Environment.lineSeparator
@@ -27,7 +28,7 @@ class BspMetalsClientSpec(
     override val protocol: BspProtocol
 ) extends BspBaseSuite {
   private val testedScalaVersion = BuildInfo.scalaVersion
-  private val semanticdbVersion = "4.8.4"
+  private val semanticdbVersion = "4.9.2"
   private val javaSemanticdbVersion = "0.5.7"
 
   private val semanticdbJar = s"semanticdb-scalac_$testedScalaVersion-$semanticdbVersion.jar"
@@ -170,7 +171,7 @@ class BspMetalsClientSpec(
 
         val scalacOptions = state.scalaOptions(`A`)._2.items.head.options
         val expectedScalacOptions = correctSourceRootOption :: List(
-          s"-Xplugin:path-to-plugin/semanticdb-scalac_2.12.18-$semanticdbVersion.jar",
+          s"-Xplugin:path-to-plugin/semanticdb-scalac_2.12.19-$semanticdbVersion.jar",
           "-Yrangepos",
           "-P:semanticdb:failures:warning",
           "-P:semanticdb:synthetics:on",
@@ -509,9 +510,9 @@ class BspMetalsClientSpec(
   //   TestUtil.withinWorkspace { workspace =>
   //     val logger = new RecordingLogger(ansiCodesSupported = false)
 
-  //     val projectName = "scala-java-processorpath"
-  //     val localScalaVersion = "2.12.18"
-  //     val localSemanticdbJar = s"semanticdb-scalac_$localScalaVersion-$semanticdbVersion.jar"
+ //     val projectName = "scala-java-processorpath"
+ //     val localScalaVersion = BuildInfo.scalaVersion
+ //     val localSemanticdbJar = s"semanticdb-scalac_$localScalaVersion-$semanticdbVersion.jar"
 
   //     val extraParams = BloopExtraBuildParams(
   //       ownsBuildFiles = None,
diff --git a/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala b/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala
index be51b1de20..51ec49ae91 100644
--- a/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala
+++ b/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala
@@ -516,6 +516,66 @@ class BspProtocolSpec(
     }
   }
 
+  test("dependency modules request works") {
+    TestUtil.withinWorkspace { workspace =>
+      val logger = new RecordingLogger(ansiCodesSupported = false)
+      loadBspBuildFromResources("cross-test-build-scalajs-0.6", workspace, logger) { build =>
+        val mainProject = build.projectFor("test-project")
+        val testProject = build.projectFor("test-project-test")
+        val mainJsProject = build.projectFor("test-projectJS")
+        val testJsProject = build.projectFor("test-projectJS-test")
+        val rootMain = build.projectFor("cross-test-build-scalajs-0-6")
+        val rootTest = build.projectFor("cross-test-build-scalajs-0-6-test")
+
+        def checkDependencyModules(project: TestProject): Unit = {
+          val dependencyModulesResult = build.state.requestDependencyModules(project)
+          assert(dependencyModulesResult.items.size == 1)
+          val dependencyModules = dependencyModulesResult.items.flatMap(item =>
+            item.modules
+              .map(dependencyModule => {
+                val json = dependencyModule.data.get
+                val mavenModule = readFromArray[bsp.MavenDependencyModule](json.value)
+                val artifacts: List[Config.Artifact] = mavenModule.artifacts
+                  .map(artifact =>
+                    Config.Artifact(
+                      dependencyModule.name,
+                      artifact.classifier,
+                      None,
+                      artifact.uri.toPath
+                    )
+                  )
+                  .distinct;
+                new Config.Module(
+                  mavenModule.organization,
+                  mavenModule.name,
+                  mavenModule.version,
+                  None,
+                  artifacts
+                )
+              })
+              .distinct
+          )
+
+          val expectedModules = project.config.resolution.toList.flatMap { res =>
+            res.modules.map { m =>
+              val artifacts = m.artifacts.map(artifact => artifact.copy(checksum = None))
+              m.copy(configurations = None, artifacts = artifacts)
+            }
+          }.distinct
+
+          assertEquals(dependencyModules, expectedModules)
+        }
+
+        checkDependencyModules(mainProject)
+        checkDependencyModules(testProject)
+        checkDependencyModules(mainJsProject)
+        checkDependencyModules(testJsProject)
+        checkDependencyModules(rootMain)
+        checkDependencyModules(rootTest)
+      }
+    }
+  }
+
   test("dependency sources request works") {
     TestUtil.withinWorkspace { workspace =>
       val logger = new RecordingLogger(ansiCodesSupported = false)
diff --git a/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala b/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala
index d1cad2896a..14e94b35a5 100644
--- a/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala
+++ b/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala
@@ -160,6 +160,7 @@ class BspSbtClientSpec(
         assertValidCompilationState(newCompiledState, projects)
 
         assertNoDiff(
+          diagnosticsWithoutTaskIds(newCompiledState, `A`),
           s"""# task start 6
              |  -> Msg: Compiling a (1 Scala source)
              |  -> Data kind: compile-task
@@ -167,56 +168,34 @@ class BspSbtClientSpec(
              |  -> errors 0, warnings 0
              |  -> origin = $secondOriginId
              |  -> Msg: Compiled 'a'
-             |  -> Data kind: compile-report""".stripMargin,
-          diagnosticsWithoutTaskIds(newCompiledState, `A`)
+             |  -> Data kind: compile-report""".stripMargin
         )
 
         assertNoDiff(
-          s"""# task start 7
-             |  -> Msg: Start no-op compilation for b
-             |  -> Data kind: compile-task
-             |# task finish 7
-             |  -> errors 0, warnings 0
-             |  -> origin = $secondOriginId
-             |  -> Msg: Compiled 'b'
-             |  -> Data kind: compile-report""".stripMargin,
-          diagnosticsWithoutTaskIds(newCompiledState, `B`)
+          diagnosticsWithoutTaskIds(newCompiledState, `B`),
+          "" // no-op
         )
 
         assertNoDiff(
-          s"""# task start 8
+          diagnosticsWithoutTaskIds(newCompiledState, `C`),
+          s"""# task start 7
              |  -> Msg: Compiling c (1 Scala source)
              |  -> Data kind: compile-task
-             |# task finish 8
+             |# task finish 7
              |  -> errors 0, warnings 0
              |  -> origin = $secondOriginId
              |  -> Msg: Compiled 'c'
-             |  -> Data kind: compile-report""".stripMargin,
-          diagnosticsWithoutTaskIds(newCompiledState, `C`)
+             |  -> Data kind: compile-report""".stripMargin
         )
 
         assertNoDiff(
-          s"""# task start 9
-             |  -> Msg: Start no-op compilation for d
-             |  -> Data kind: compile-task
-             |# task finish 9
-             |  -> errors 0, warnings 0
-             |  -> origin = $secondOriginId
-             |  -> Msg: Compiled 'd'
-             |  -> Data kind: compile-report""".stripMargin,
-          diagnosticsWithoutTaskIds(newCompiledState, `D`)
+          diagnosticsWithoutTaskIds(newCompiledState, `D`),
+          "" // no-op
         )
 
         assertNoDiff(
-          s"""# task start 10
-             |  -> Msg: Start no-op compilation for e
-             |  -> Data kind: compile-task
-             |# task finish 10
-             |  -> errors 0, warnings 0
-             |  -> origin = $secondOriginId
-             |  -> Msg: Compiled 'e'
-             |  -> Data kind: compile-report""".stripMargin,
-          diagnosticsWithoutTaskIds(newCompiledState, `E`)
+          diagnosticsWithoutTaskIds(newCompiledState, `E`),
+          "" // no-op
         )
 
         assertSameExternalClassesDirs(newCompiledState, initialStateBackup, List(`B`, `D`, `E`))
@@ -307,17 +286,9 @@ class BspSbtClientSpec(
 
         assertNoDiff(
           thirdCompiledState.lastDiagnostics(`A`),
-          """
-            |#3: task start 3
-            |  -> Msg: Start no-op compilation for a
-            |  -> Data kind: compile-task
-            |#3: a/src/main/scala/Foo.scala
+          """#3: a/src/main/scala/Foo.scala
             |  -> List()
-            |  -> reset = true
-            |#3: task finish 3
-            |  -> errors 0, warnings 0
-            |  -> Msg: Compiled 'a'
-            |  -> Data kind: compile-report """.stripMargin
+            |  -> reset = true""".stripMargin
         )
       }
     }
diff --git a/frontend/src/test/scala/bloop/bsp/TestConstants.scala b/frontend/src/test/scala/bloop/bsp/TestConstants.scala
index db3b4359b2..f3699b62e6 100644
--- a/frontend/src/test/scala/bloop/bsp/TestConstants.scala
+++ b/frontend/src/test/scala/bloop/bsp/TestConstants.scala
@@ -16,6 +16,7 @@ object TestConstants {
         "debugProvider": { "languageIds": ["scala", "java"] },
         "inverseSourcesProvider": true,
         "dependencySourcesProvider": true,
+        "dependencyModulesProvider": true,
         "resourcesProvider": true,
         "buildTargetChangedProvider": false,
         "jvmRunEnvironmentProvider": true,
diff --git a/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala b/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala
index c51ec9edba..0aa7154506 100644
--- a/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala
+++ b/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala
@@ -14,12 +14,7 @@ import bloop.task.Task
 
 import com.microsoft.java.debug.core.protocol.Events
 import com.microsoft.java.debug.core.protocol.Requests._
-import com.microsoft.java.debug.core.protocol.Responses.ContinueResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.EvaluateResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.ScopesResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.SetBreakpointsResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.StackTraceResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.VariablesResponseBody
+import com.microsoft.java.debug.core.protocol.Responses._
 import com.microsoft.java.debug.core.protocol.Types.Capabilities
 import monix.execution.Cancelable
 import monix.execution.Scheduler
@@ -119,6 +114,11 @@ private[dap] final class DebugAdapterConnection(
     adapter.request(Attach, arguments)
   }
 
+  def redefineClasses(): Task[RedefineClassesResponse] = {
+    val args = new RedefineClassesArguments()
+    adapter.request(RedefineClasses, args)
+  }
+
   def close(): Unit = {
     try socket.close()
     finally {
diff --git a/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala b/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala
index e27b1527e6..06f18d71e2 100644
--- a/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala
+++ b/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala
@@ -1,33 +1,40 @@
 package bloop.dap
 
+import java.io.Closeable
 import java.net.ConnectException
 import java.net.SocketException
 import java.net.SocketTimeoutException
 import java.util.NoSuchElementException
 import java.util.concurrent.TimeUnit.MILLISECONDS
-import java.util.concurrent.TimeUnit.SECONDS
 
+import scala.collection.JavaConverters._
 import scala.collection.mutable
 import scala.concurrent.Future
 import scala.concurrent.Promise
 import scala.concurrent.TimeoutException
 import scala.concurrent.duration.Duration
 import scala.concurrent.duration.FiniteDuration
+import scala.concurrent.duration._
 
+import ch.epfl.scala.bsp
 import ch.epfl.scala.bsp.ScalaMainClass
 import ch.epfl.scala.debugadapter._
 
+import bloop.Cli
 import bloop.ScalaInstance
-import ch.epfl.scala.bsp
+import bloop.cli.CommonOptions
 import bloop.cli.ExitStatus
 import bloop.data.Platform
 import bloop.data.Project
+import bloop.engine.NoPool
 import bloop.engine.State
 import bloop.engine.tasks.RunMode
 import bloop.engine.tasks.Tasks
+import bloop.internal.build
 import bloop.internal.build.BuildTestInfo
 import bloop.io.AbsolutePath
 import bloop.io.Environment.lineSeparator
+import bloop.logging.BspClientLogger
 import bloop.logging.Logger
 import bloop.logging.LoggerAction
 import bloop.logging.LoggerAction.LogInfoMessage
@@ -42,6 +49,8 @@ import bloop.util.TestUtil
 import com.microsoft.java.debug.core.protocol.Requests.SetBreakpointArguments
 import com.microsoft.java.debug.core.protocol.Types
 import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint
+import coursierapi.Dependency
+import coursierapi.Fetch
 import monix.execution.Ack
 import monix.reactive.Observer
 
@@ -50,11 +59,6 @@ object DebugServerSpec extends DebugBspBaseSuite {
   private val Success: ExitStatus = ExitStatus.Ok
   private val resolver = new BloopDebugToolsResolver(NoopLogger)
 
-  override def test(name: String)(fun: => Any): Unit =
-    super.test(name) {
-      TestUtil.retry()(fun)
-    }
-
   testTask("cancelling server closes server connection", FiniteDuration(10, SECONDS)) {
     startDebugServer(Task.now(Success)) { server =>
       for {
@@ -207,6 +211,81 @@ object DebugServerSpec extends DebugBspBaseSuite {
     }
   }
 
+  testTask(
+    "runs-correct-runtime",
+    FiniteDuration(60, SECONDS)
+  ) {
+    TestUtil.withinWorkspace { workspace =>
+      val runtimeClasspath = Fetch
+        .create()
+        .addDependencies(
+          Dependency.of(
+            "ch.qos.logback",
+            "logback-classic",
+            "1.2.7"
+          ),
+          Dependency.of(
+            "org.scala-lang",
+            "scala-library",
+            build.BuildInfo.scalaVersion
+          )
+        )
+        .fetch()
+        .asScala
+        .map(_.toPath())
+        .toList
+
+      val main =
+        """|/main/scala/Main.scala
+           |object Main {
+           |  def main(args: Array[String]): Unit = {
+           |    println(s">>> hello world! <<<")
+           |    val cn = Class.forName("ch.qos.logback.classic.Logger")
+           |    println(s"$cn")
+           |  }
+           |}
+           |
+           |""".stripMargin
+
+      val logger = new RecordingLogger(ansiCodesSupported = false)
+      val project =
+        TestProject(workspace, "r", List(main), runtimeClasspath = Some(runtimeClasspath))
+
+      loadBspStateWithTask(workspace, List(project), logger) { state =>
+        val runner = mainRunner(
+          project,
+          state,
+          arguments = Nil,
+          jvmOptions = Nil,
+          environmentVariables = Nil
+        )
+
+        startDebugServer(runner) { server =>
+          for {
+            client <- server.startConnection
+            _ <- client.initialize()
+            _ <- client.launch(noDebug = true)
+            _ <- client.configurationDone()
+            _ <- client.exited
+            _ <- client.terminated
+            _ <- Task.fromFuture(client.closedPromise.future)
+            output <- client.takeCurrentOutput
+          } yield {
+            assert(client.socket.isClosed)
+            assertNoDiff(
+              output.linesIterator
+                .filterNot(_.contains("ERROR: JDWP Unable to get JNI 1.2 environment"))
+                .filterNot(_.contains("JDWP exit error AGENT_ERROR_NO_JNI_ENV"))
+                .mkString(lineSeparator),
+              """|>>> hello world! <<<
+                 |class ch.qos.logback.classic.Logger""".stripMargin
+            )
+          }
+        }
+      }
+    }
+  }
+
   testTask("supports scala and java breakpoints", FiniteDuration(60, SECONDS)) {
     TestUtil.withinWorkspace { workspace =>
       object Sources {
@@ -541,9 +620,9 @@ object DebugServerSpec extends DebugBspBaseSuite {
 
         val attachRemoteProcessRunner =
           BloopDebuggeeRunner.forAttachRemote(
+            Seq(buildProject),
             state.compile(project).toTestState.state,
-            defaultScheduler,
-            Seq(buildProject)
+            defaultScheduler
           )
 
         startDebugServer(attachRemoteProcessRunner) { server =>
@@ -784,9 +863,9 @@ object DebugServerSpec extends DebugBspBaseSuite {
 
         val attachRemoteProcessRunner =
           BloopDebuggeeRunner.forAttachRemote(
+            Seq(buildProject),
             testState.state,
-            defaultScheduler,
-            Seq(buildProject)
+            defaultScheduler
           )
 
         startDebugServer(attachRemoteProcessRunner) { server =>
@@ -890,6 +969,87 @@ object DebugServerSpec extends DebugBspBaseSuite {
     }
   }
 
+  testTask("hot code replace", 30.seconds) {
+    val mainSource =
+      """|/Main.scala
+         |object Main {
+         |  def main(args: Array[String]): Unit = {
+         |    val a = new example.A
+         |    a.m()
+         |  }
+         |}
+         |""".stripMargin
+    val originalSource =
+      """|/example/A.scala
+         |package example
+         |class A {
+         |  def m() = {
+         |    println("A")
+         |  }
+         |}
+         |""".stripMargin
+    val modifiedSource =
+      """|/example/A.scala
+         |package example
+         |class A {
+         |  def m() = {
+         |    println("B")
+         |  }
+         |}
+         |""".stripMargin
+    val logger = new RecordingLogger(ansiCodesSupported = false)
+    TestUtil.withinWorkspace { workspace =>
+      val dependency = TestProject(workspace, "a", List(originalSource))
+      val mainProject = TestProject(workspace, "main", List(mainSource), List(dependency))
+      val configDir = TestProject.populateWorkspace(workspace, List(mainProject, dependency))
+
+      def cliCompile(project: TestProject) = {
+        val compileArgs = Array("compile", project.config.name, "--config-dir", configDir.syntax)
+        val compileAction = Cli.parse(compileArgs, CommonOptions.default)
+        Task.eval(Cli.run(compileAction, NoPool)).executeAsync
+      }
+
+      def bspCommand() = createBspCommand(configDir)
+      val state = TestUtil.loadTestProject(configDir.underlying, logger)
+      openBspConnection(state, bspCommand, configDir, new BspClientLogger(logger))
+        .withinSession { state =>
+          val testState = state.compile(mainProject).toTestState
+          val `A.scala` = testState
+            .getProjectFor(dependency)
+            .sources
+            .map(_.resolve("example/A.scala"))
+            .find(_.exists)
+            .get
+
+          val runner = mainRunner(mainProject, state)
+          startDebugServer(runner) { server =>
+            for {
+              client <- server.startConnection
+              _ <- client.initialize()
+              _ <- client.launch(noDebug = false)
+              _ <- client.initialized
+              response <- client.setBreakpoints(breakpointsArgs(`A.scala`, 4))
+              _ = assert(response.breakpoints.forall(_.verified))
+              _ <- client.configurationDone()
+              stopped <- client.stopped
+              _ = writeFile(`A.scala`, modifiedSource)
+              _ <- cliCompile(mainProject) // another client trigger a compilation
+              _ = state.compile(mainProject) // noop
+              _ <- client.redefineClasses()
+              _ <- client.continue(stopped.threadId)
+              _ <- client.exited
+              _ <- client.terminated
+              _ <- Task.fromFuture(client.closedPromise.future)
+              output <- client.takeCurrentOutput
+            } yield {
+              assert(client.socket.isClosed)
+              assertNoDiff(output, "B")
+            }
+          }
+        }
+    }
+  }
+
   private def startRemoteProcess(buildProject: Project, testState: TestState): Task[Int] = {
     val attachPort = Promise[Int]()
 
@@ -1025,9 +1185,11 @@ object DebugServerSpec extends DebugBspBaseSuite {
       override def javaRuntime: Option[JavaRuntime] = None
       def name: String = "MockRunner"
       def run(listener: DebuggeeListener): CancelableFuture[Unit] = {
-        DapCancellableFuture.runAsync(task.map(_ => ()), defaultScheduler)
+        DapCancellableFuture.runAsync(task.map(_ => ()))(defaultScheduler)
       }
       def scalaVersion: ScalaVersion = ScalaVersion("2.12.17")
+
+      override def observeClassUpdates(onClassUpdate: Seq[String] => Unit): Closeable = () => ()
     }
 
     startDebugServer(
@@ -1040,7 +1202,7 @@ object DebugServerSpec extends DebugBspBaseSuite {
 
   def startDebugServer(
       debuggee: Debuggee,
-      gracePeriod: Duration = Duration(5, SECONDS)
+      gracePeriod: Duration = 5.seconds
   )(f: TestServer => Task[Unit]): Task[Unit] = {
     val logger = new RecordingLogger(ansiCodesSupported = false)
     val dapLogger = new DebugServerLogger(logger)
@@ -1067,7 +1229,7 @@ object DebugServerSpec extends DebugBspBaseSuite {
     override def close(): Unit = {
       cancel()
       val allClientsClosed = clients.map(c => Task.fromFuture(c.closedPromise.future))
-      TestUtil.await(10, SECONDS)(Task.sequence(allClientsClosed)); ()
+      TestUtil.await(10.seconds)(Task.sequence(allClientsClosed)); ()
     }
 
     def startConnection: Task[DebugAdapterConnection] = Task {
diff --git a/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala b/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala
index 158ac70f81..275fc620ad 100644
--- a/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala
+++ b/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala
@@ -4,12 +4,7 @@ import bloop.dap.DebugTestProtocol._
 
 import com.microsoft.java.debug.core.protocol.Events
 import com.microsoft.java.debug.core.protocol.Requests._
-import com.microsoft.java.debug.core.protocol.Responses.ContinueResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.EvaluateResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.ScopesResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.SetBreakpointsResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.StackTraceResponseBody
-import com.microsoft.java.debug.core.protocol.Responses.VariablesResponseBody
+import com.microsoft.java.debug.core.protocol.Responses._
 import com.microsoft.java.debug.core.protocol.Types
 
 private[dap] object DebugTestEndpoints {
@@ -24,6 +19,8 @@ private[dap] object DebugTestEndpoints {
   val Variables = new Request[VariablesArguments, VariablesResponseBody]("variables")
   val Evaluate = new Request[EvaluateArguments, EvaluateResponseBody]("evaluate")
   val Continue = new Request[ContinueArguments, ContinueResponseBody]("continue")
+  val RedefineClasses =
+    new Request[RedefineClassesArguments, RedefineClassesResponse]("redefineClasses")
   val ConfigurationDone = new Request[Unit, Unit]("configurationDone")
 
   val Exited = new Event[Events.ExitedEvent]("exited")
diff --git a/frontend/src/test/scala/bloop/testing/BloopHelpers.scala b/frontend/src/test/scala/bloop/testing/BloopHelpers.scala
index e668381ae3..61a5899bc2 100644
--- a/frontend/src/test/scala/bloop/testing/BloopHelpers.scala
+++ b/frontend/src/test/scala/bloop/testing/BloopHelpers.scala
@@ -21,6 +21,7 @@ import bloop.io.ParallelOps
 import bloop.io.ParallelOps.CopyMode
 import bloop.io.RelativePath
 import bloop.logging.Logger
+import bloop.logging.NoopLogger
 import bloop.logging.RecordingLogger
 import bloop.task.Task
 import bloop.util.TestProject
@@ -28,7 +29,6 @@ import bloop.util.TestUtil
 
 import monix.execution.CancelableFuture
 import monix.execution.Scheduler
-import bloop.logging.NoopLogger
 
 trait BloopHelpers {
   def loadState(
diff --git a/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala b/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala
index 149aa7324d..cb185f9f35 100644
--- a/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala
+++ b/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala
@@ -2,11 +2,12 @@ package bloop.testing
 
 import java.nio.file.Files
 
+import scala.concurrent.duration.Duration
+
 import bloop.io.AbsolutePath
 import bloop.io.Paths
 import bloop.logging.RecordingLogger
 import bloop.task.Task
-import scala.concurrent.duration.Duration
 
 class ProjectBaseSuite(buildName: String) extends BaseSuite {
   val workspace: AbsolutePath = AbsolutePath(Files.createTempDirectory(s"workspace-${buildName}"))
diff --git a/frontend/src/test/scala/bloop/util/TestProject.scala b/frontend/src/test/scala/bloop/util/TestProject.scala
index 81811d907c..b55ba3a387 100644
--- a/frontend/src/test/scala/bloop/util/TestProject.scala
+++ b/frontend/src/test/scala/bloop/util/TestProject.scala
@@ -107,6 +107,7 @@ abstract class BaseTestProject {
       scalaVersion: Option[String] = None,
       resources: List[String] = Nil,
       runtimeResources: Option[List[String]] = None,
+      runtimeClasspath: Option[List[Path]] = None,
       jvmConfig: Option[Config.JvmConfig] = None,
       runtimeJvmConfig: Option[Config.JvmConfig] = None,
       order: Config.CompileOrder = Config.Mixed,
@@ -136,7 +137,7 @@ abstract class BaseTestProject {
       mkScalaInstance(finalScalaOrg, finalScalaCompiler, scalaVersion, jars.toList, NoopLogger)
 
     val allJars = instance.allJars.map(AbsolutePath.apply)
-    val (compileClasspath, runtimeClasspath) = {
+    val (compileClasspath, runtimeClasspathForStrict) = {
       val transitiveClasspath =
         (directDependencies.flatMap(classpathDeps) ++ allJars ++ jars).map(_.underlying)
       val directClasspath =
@@ -170,7 +171,7 @@ abstract class BaseTestProject {
       javaConfig,
       None,
       runtimeJvmConfig,
-      Some(runtimeClasspath),
+      runtimeClasspath.orElse(Some(runtimeClasspathForStrict)),
       runtimeResourcesList
     )
 
diff --git a/shared/src/main/scala/bloop/io/AbsolutePath.scala b/shared/src/main/scala/bloop/io/AbsolutePath.scala
index 26eab39a18..7faff8220c 100644
--- a/shared/src/main/scala/bloop/io/AbsolutePath.scala
+++ b/shared/src/main/scala/bloop/io/AbsolutePath.scala
@@ -6,6 +6,9 @@ import java.net.URI
 import java.nio.file.Files
 import java.nio.file.Path
 import java.nio.file.{Paths => NioPaths}
+import java.util.stream.Collectors
+
+import scala.collection.JavaConverters._
 
 final class AbsolutePath private (val underlying: Path) extends AnyVal {
   def syntax: String = toString
@@ -21,6 +24,7 @@ final class AbsolutePath private (val underlying: Path) extends AnyVal {
   def getParent: AbsolutePath = AbsolutePath(underlying.getParent)
   def createDirectories: AbsolutePath = AbsolutePath(Files.createDirectories(underlying))
   def exists: Boolean = Files.exists(underlying)
+  def list: List[Path] = Files.list(underlying).collect(Collectors.toList()).asScala.toList
   def isFile: Boolean = Files.isRegularFile(underlying)
   def isDirectory: Boolean = Files.isDirectory(underlying)
   def isSameFile(other: AbsolutePath): Boolean = Files.isSameFile(underlying, other.underlying)
diff --git a/shared/src/main/scala/bloop/logging/Logger.scala b/shared/src/main/scala/bloop/logging/Logger.scala
index dd2df7ae3c..19497d7603 100644
--- a/shared/src/main/scala/bloop/logging/Logger.scala
+++ b/shared/src/main/scala/bloop/logging/Logger.scala
@@ -2,6 +2,8 @@ package bloop.logging
 
 import java.util.function.Supplier
 
+import bloop.io.Environment
+
 abstract class Logger extends xsbti.Logger with BaseSbtLogger {
 
   /** The name of the logger */
@@ -31,7 +33,12 @@ abstract class Logger extends xsbti.Logger with BaseSbtLogger {
   override def info(msg: Supplier[String]): Unit = info(msg.get)
   override def trace(exception: Supplier[Throwable]): Unit = trace(exception.get())
 
-  def report(msg: String, t: Throwable): Unit = { error(msg); trace(t) }
+  def error(msg: String, t: Throwable): Unit = {
+    error(
+      msg + Environment.lineSeparator + Logger.prettyPrintException(t)
+    )
+  }
+
   def handleCompilationEvent(): Unit = ()
 
   /** Display a message as a warning to user using `showMessage` in BSP-based loggers and `warn` otherwise. */
@@ -48,3 +55,12 @@ private[logging] trait BaseSbtLogger extends sbt.testing.Logger {
   private[logging] def printDebug(line: String): Unit
   override def debug(msg: String): Unit = printDebug(msg)
 }
+
+object Logger {
+  def prettyPrintException(t: Throwable): String = {
+    val sw = new java.io.StringWriter()
+    val pw = new java.io.PrintWriter(sw)
+    t.printStackTrace(pw)
+    sw.toString()
+  }
+}