I've sometimes used a snippet like this:
(defun rebuild (&optional (sha1 (sha1-of-current-commit)))
(asdf:operate 'asdf:compile-op :my-package)
(asdf:operate 'asdf:load-op :my-package)
(push sha1 *sha1s-of-loaded-commits*))
(defun rebuild-if-necessary ()
(let ((sha1 (getf (current-commit) :sha1))
(loaded (car *sha1s-of-loaded-commits*)))
(unless (equal sha1 loaded)
(format t "Rebuilding since ~S differs from ~S.~%"
sha1 loaded)
(rebuild sha1)
t)))
And then in for example a web service project, I would just call that function in the basic request dispatch handler.
When a web request comes in to the Lisp system, it will check the working directory's Git repository. If the HEAD points to a new commit, it will compile the new code (to machine code, with optimizations!) and reload the whole web app package. When the REBUILD-IF-NECESSARY function call returns, the new code is already loaded, and the web request proceeds as normal, but with the new code loaded.
And yeah, the conditions system is really radically better than exceptions. Raising a condition doesn't unwind the stack, but just calls the closest condition handler, more like a function call than an exception. The handler is offered a choice of restart opportunities, so for example a FILE-NOT-FOUND condition might offer to restart with a different file path, to just retry, or to error. Having restarts available for almost all exceptional conditions makes the system much more flexible.