An sbt 2.x plugin that auto-restarts your Scala application on source changes. Like sbt-revolver, but built on sbt 2.x primitives.
Add to project/plugins.sbt:
addSbtPlugin("com.jamesward" % "sbt-reload" % "<version>")No other configuration needed — the plugin auto-activates on all JVM projects.
./sbt ~runReload
For a Test-scoped main:
./sbt ~Test/runReload
This will:
- Compile and start your app in a forked JVM
- Watch for source changes
- On change: stop the running app, recompile, restart
- Press Enter to exit watch mode (stops the app)
Because sbt 2.x runs a persistent server shared by every connected client, one client
can pause and later resume another client's running ~runReload. While a scope is
paused, runReload invocations for that project/config keep the current fork running and
do not restart it, even when sources change:
./sbt reloadPause # pause Compile-scoped runReload
./sbt Test/reloadPause # pause Test-scoped runReload
./sbt reloadResume # resume it
This is useful when a second client (for example an AI agent) is driving edits and wants
to make several changes without the watching ~runReload restarting the app on each one.
The agent calls reloadPause, makes its edits, then calls reloadResume.
Semantics:
- Pause/resume are scoped per project and per config, exactly like
runReload. Issuing them from an aggregate root pauses/resumes every aggregated subproject's scope. - While paused, source changes are recompiled but the fork is not restarted, so the running app keeps serving old bytecode until you resume.
- After
reloadResume, the nextrunReload(the next~runReloadtrigger, or a manual invocation) restarts the fork if its inputs changed while paused. Resuming does not itself force a restart — it re-enables the normal change-driven restart. - Pause state is cleared automatically when the
~runReloadwatch exits (watchOnTermination) and on sbtreload/exit(onUnload).
reloadStatus reports, for a scope (project + config), whether a runReload fork is
currently running and whether it is paused:
./sbt reloadStatus # Compile scope
./sbt Test/reloadStatus # Test scope
It prints one of:
not running— norunReloadfork is alive for this scoperunning— a fork is alive and not pausedrunning (paused)— a fork is alive but paused viareloadPause
Like reloadOutput and reloadPause/reloadResume, it reads the shared sbt server
state, so it reflects a fork or pause started by any connected client.
sbt 2.x runs a persistent server that multiple clients can connect to. When one
client runs ~runReload, its forked app's output is also captured to a
per-config file so a different client can view it read-only:
./sbt reloadOutput # Compile-scoped runReload output
./sbt Test/reloadOutput # Test-scoped runReload output
reloadOutput prints whatever the running app has emitted since the last call
and returns immediately (it does not stream/block, so it never starves the
shared server's command loop). An agent can poll it after triggering a rebuild
to see what the app printed. The capture file is truncated on each restart, and
reloadOutput resets accordingly — so the first output of a freshly restarted
fork is shown in full, even if it is longer than what the previous fork emitted.
reloadOutput is project-aware, keyed off the live background-job service
rather than its own task axis. When the project you invoke it in has its own
running runReload fork (e.g. myapp/reloadOutput, or bare reloadOutput in a
single-project build), it shows only that project's output. Only when the
invoking scope has no fork of its own — typically an aggregate root that just
aggregates subprojects — does it fall back to reporting every running fork in
the config. So in a multi-project build you can still run reloadOutput from the
aggregate root and see whichever subproject's fork is running, without a
subproject-scoped call dumping every other subproject's output. When more than
one fork is reported (the aggregate-root fallback), each line is prefixed with the
project id. The config axis is always honored: reloadOutput shows Compile forks,
Test/reloadOutput shows Test forks.
When no fork has produced anything since the last call, reloadOutput prints a
single reloadOutput: no new output line — not one line per running fork. This
keeps a streaming ~reloadOutput quiet (it only prints real output) while still
giving a manual one-shot poll a single confirmation that there was nothing new.
Do not run
~reloadOutput. Use the one-shotreloadOutput(poll it after triggering a rebuild); do not run it under sbt's watch.sbt has a limitation where two concurrent continuous (
~) builds on one server share a single file-watch repository, and after the first triggered rebuild one of the two watch sessions stops receiving further file-change events (the session started first goes deaf). This is not specific to this plugin — it reproduces with two plain~compilesessions and no plugin involved — but running~reloadOutputalongside~runReloadis a natural way to hit it, and the casualty is usually~runReload, which silently stops restarting on edits.The intended pattern for "watch + observe from another client": run one
~runReloadwatch, and from the other client poll the one-shotreloadOutput(it is a non-blocking poll by design — see above). A single~session restarts reliably on every change, and each one-shotreloadOutputcall returns whatever the app has emitted since the last poll.
runReload is scoped per project and per config, so each
<project>/<config>/runReload manages its own forked JVM and its own capture
file. This has two consequences:
- Aggregated
~runReload(run from a project that aggregates others) restarts only the subproject(s) whose inputs actually changed. Each scope keeps a content fingerprint of its classpath,run / mainClass, andrunReloadArgs; if a scope's fingerprint is unchanged and its fork is still alive, that scope takes a no-op path instead of restarting. - Stopping is isolated to the matching scope. Exiting
~runReloadstops only that scope's fork (matched by project + config), not other subprojects'.reload/exitstop everyrunReloadfork across the build.
Uses run / mainClass by default. Override if needed:
run / mainClass := Some("com.example.MyApp")Or for Test scope:
Test / run / mainClass := Some("com.example.TestServer")run / javaOptions ++= Seq("-Xmx512m", "-Dconfig.file=dev.conf")runReloadArgs := Seq("--port", "8080")run / envVars := Map("DATABASE_URL" -> "jdbc:postgresql://localhost/dev")- Uses sbt's
BackgroundJobServiceto manage the forked process lifecycle - Stops prior
runReloadjobs before starting a new one (single-instance per project/config by default) - On compile failure, the running app keeps serving — no restart until the build succeeds
- On watch exit (Enter or client disconnect), only the forked process for the
exiting scope is terminated via
watchOnTermination, matched by project + config so other subprojects' forks keep running - On sbt
reloadorexit, cleanup runs viaonUnload, stopping everyrunReloadfork across the build reloadOutputis fed byrunReload, which wraps the fork's background logger in a tee that writes each line totarget/reload/<config>-output.log(under sbt 2.x's per-project outputtarget, not<base>/target). The live~runReloadview is unaffected because the tee still forwards to the original logger.reloadOutputreads new bytes since its last call (line-aligned), and resets to the start of the file wheneverrunReloadrestarts the fork (which truncates the capture file), so it is a non-blocking poll rather than a stream.
- sbt 2.0.0+
Apache-2.0