Yes, in the post I didn't get too technical with the treatment of tasks; I'll elaborate a little here. First, the command line thing.

You're right that the command line isn't strictly required to use boot, you can do everything at the REPL or perl/awk etc., as you pointed out. But for me it's really useful just ergonomically to be able to use command line arguments to configure ad-hoc builds because they can be very concise. Just like I probably wouldn't be super excited to use a Clojure shell instead of Bash, because Lisp is usually more verbose for the kinds of things I do on the command line. Consider:

$ boot cljs -usO none

vs

(boot (cljs :unified true :source-map true :optimizations :none))

the command line version is just nicer for that. When it's time to automate is when you'd put that in your build.boot and make it a new task.

This brings us to tasks. We used to describe them as "middleware factories" but Rich has provided us with a way cooler name: stateful transducers. The build process can be imagined as a transducer stack applied to a file set instead of to an async channel or sequential thing. The principal value the task abstraction provides is their process-building power.

A typical task definition looks like this:

(deftask foo
"This task does foo."
[...] ; kwargs/cli-opts
(let [state ...] ; local state
(fn [continue] ; middleware
(fn [event] ; handler
... ; build something, do work
(continue event) ; call continuation
))))

Please ignore "event", it's there for historical reasons. But like transducers, we have powerful ways to build processes from tasks that don't need to know anything about each other now. For example:

(deftask bar
"This task does bar."
[...]
(comp
(fn [continue]
(fn [event]
...
(continue event)))
(foo :bar "baz" :baf "quux")
(omg :hello "world")))

A key property of transducers is that they can also perform process control flow duties. The boot `watch` task, for instance, is a totally general-purpose way to to incremental-anything in boot. The `cljs` task doesn't have a file watcher in it, none of the other tasks do. They don't need it.

Another example is the `cljs-repl` task, which emits ClojureScript code when you start the CLJS REPL. This requires recompiling the JS file and reloading the client. This all happens automatically because the cljs-repl task can call its continuation whenever it likes, so it does that when you start the REPL. This means that your webapp code doesn't contain any REPL connecting code, so you don't have to think about removing it for production builds etc. The REPL connecting code is in there when you use the task, and not when you don't. Very clean.

Another interesting property of tasks is that they accept only keyword arguments. They do not take positional parameters. This means that partial application of tasks is idempotent, and that last-setting wins. For instance, given a function f that takes no positional parameters, we have:

(-> f (partial :foo "bar")) ==
(-> f (partial :foo "bar") (partial :foo "bar"))

and

(-> f (partial :foo "bar")) ==
(-> f (partial :foo "baz") (partial :foo "bar")).

This is pretty interesting because it gives us a nice way to manage global preferences. We have a macro called task-options! which can be used to globally apply options to tasks:

(task-options!
foo [:bar "baz"
:baf "quux"]
omg [:hello "world"])

This macro actually does some currying and alter-var-root, replacing the value of the task var (deftask defines a var, of course) with a curried version. A cool thing about this is that the last-setting-wins property means that you can override these settings on the command line or in the REPL:

(boot (foo :bar "not-baz") (omg))

which would override the :bar option, but not the others.

This is probably long enough, hahaha! I'll hand the mic back to you now :)