feat: add clean command
diff --git a/cmd/aspect/build/build.go b/cmd/aspect/build/build.go
index 670654b..8d9dbac 100644
--- a/cmd/aspect/build/build.go
+++ b/cmd/aspect/build/build.go
@@ -31,7 +31,7 @@
 
 	cmd := &cobra.Command{
 		Use:   "build",
-		Short: "Builds the specified targets, using the options.",
+		Short: "Builds the specified targets, using the options",
 		Long: "Invokes bazel build on the specified targets. " +
 			"See 'bazel help target-syntax' for details and examples on how to specify targets to build.",
 		RunE: b.Run,
diff --git a/cmd/aspect/clean/BUILD.bazel b/cmd/aspect/clean/BUILD.bazel
new file mode 100644
index 0000000..df6edaf
--- /dev/null
+++ b/cmd/aspect/clean/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "clean",
+    srcs = ["clean.go"],
+    importpath = "aspect.build/cli/cmd/aspect/clean",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/aspect/clean",
+        "//pkg/bazel",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
diff --git a/cmd/aspect/clean/clean.go b/cmd/aspect/clean/clean.go
new file mode 100644
index 0000000..1a52528
--- /dev/null
+++ b/cmd/aspect/clean/clean.go
@@ -0,0 +1,77 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package clean
+
+import (
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/pkg/aspect/clean"
+	"aspect.build/cli/pkg/bazel"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+// NewDefaultCleanCmd creates a new clean cobra command with the default
+// dependencies.
+func NewDefaultCleanCmd() *cobra.Command {
+	return NewCleanCmd(ioutils.DefaultStreams, bazel.New())
+}
+
+// NewCleanCmd creates a new clean cobra command.
+func NewCleanCmd(
+	streams ioutils.Streams,
+	bzl bazel.Spawner,
+) *cobra.Command {
+	b := clean.New(streams, bzl)
+
+	cmd := &cobra.Command{
+		Use:   "clean",
+		Short: "Removes the output tree",
+		Long: `Removes bazel-created output, including all object files, and bazel metadata.
+
+clean deletes the output directories for all build configurations performed by
+this Bazel instance, or the entire working tree created by this Bazel instance,
+and resets internal caches.
+
+If executed without any command-line options, then the output directory for all
+configurations will be cleaned.
+
+Recall that each Bazel instance is associated with a single workspace,
+thus the clean command will delete all outputs from all builds you've
+done with that Bazel instance in that workspace.
+
+NOTE: clean is primarily intended for reclaiming disk space for workspaces
+that are no longer needed.
+It causes all subsequent builds to be non-incremental.
+If this is not your intent, consider these alternatives:
+
+Do a one-off non-incremental build:
+	bazel --output_base=$(mktemp -d) ...
+
+Force repository rules to re-execute:
+	bazel sync --configure
+
+Workaround inconistent state:
+	Bazel's incremental rebuilds are designed to be correct, so clean
+	should never be required due to inconsistencies in the build.
+	Such problems are fixable and these bugs are a high priority.
+	If you ever find an incorrect incremental build, please file a bug report,
+	and only use clean as a temporary workaround.`,
+		RunE: b.Run,
+	}
+
+	cmd.PersistentFlags().BoolVarP(&b.Expunge, "expunge", "", false, `Remove the entire output_base tree.
+This removes all build output, external repositories,
+and temp files created by Bazel.
+It also stops the Bazel server after the clean,
+equivalent to the shutdown command.`)
+
+	cmd.PersistentFlags().BoolVarP(&b.ExpungeAsync, "expunge_async", "", false, `Expunge in the background.
+It is safe to invoke a Bazel command in the same
+workspace while the asynchronous expunge continues to run.
+Note, however, that this may introduce IO contention.`)
+	return cmd
+}
diff --git a/cmd/aspect/docs/docs.go b/cmd/aspect/docs/docs.go
index 91612ca..fd6359b 100644
--- a/cmd/aspect/docs/docs.go
+++ b/cmd/aspect/docs/docs.go
@@ -24,8 +24,8 @@
 		Use:   "docs",
 		Short: "Open documentation in the browser",
 		Long: `Given a selected topic, open the relevant API docs in a browser window.
-		The mechanism of choosing the browser to open is documented at https://github.com/pkg/browser
-		By default, opens docs.bazel.build`,
+The mechanism of choosing the browser to open is documented at https://github.com/pkg/browser
+By default, opens docs.bazel.build`,
 		RunE: v.Run,
 	}
 
diff --git a/cmd/aspect/root/BUILD.bazel b/cmd/aspect/root/BUILD.bazel
index b7c3b24..d186a75 100644
--- a/cmd/aspect/root/BUILD.bazel
+++ b/cmd/aspect/root/BUILD.bazel
@@ -10,6 +10,7 @@
     ],
     deps = [
         "//cmd/aspect/build",
+        "//cmd/aspect/clean",
         "//cmd/aspect/docs",
         "//cmd/aspect/info",
         "//cmd/aspect/version",
diff --git a/cmd/aspect/root/root.go b/cmd/aspect/root/root.go
index 5b3213e..608513e 100644
--- a/cmd/aspect/root/root.go
+++ b/cmd/aspect/root/root.go
@@ -16,6 +16,7 @@
 	"github.com/spf13/viper"
 
 	"aspect.build/cli/cmd/aspect/build"
+	"aspect.build/cli/cmd/aspect/clean"
 	"aspect.build/cli/cmd/aspect/docs"
 	"aspect.build/cli/cmd/aspect/info"
 	"aspect.build/cli/cmd/aspect/version"
@@ -69,6 +70,7 @@
 	// ### Child commands
 	// IMPORTANT: when adding a new command, also update the _DOCS list in /docs/BUILD.bazel
 	cmd.AddCommand(build.NewDefaultBuildCmd())
+	cmd.AddCommand(clean.NewDefaultCleanCmd())
 	cmd.AddCommand(version.NewDefaultVersionCmd())
 	cmd.AddCommand(docs.NewDefaultDocsCmd())
 	cmd.AddCommand(info.NewDefaultInfoCmd())
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 3abc99d..95a8cc4 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -5,6 +5,7 @@
 _DOCS = [
     "aspect.md",
     "aspect_build.md",
+    "aspect_clean.md",
     "aspect_docs.md",
     "aspect_info.md",
     "aspect_version.md",
diff --git a/docs/aspect.md b/docs/aspect.md
index 69358f2..d86bc38 100644
--- a/docs/aspect.md
+++ b/docs/aspect.md
@@ -16,7 +16,8 @@
 
 ### SEE ALSO
 
-* [aspect build](aspect_build.md)	 - Builds the specified targets, using the options.
+* [aspect build](aspect_build.md)	 - Builds the specified targets, using the options
+* [aspect clean](aspect_clean.md)	 - Removes the output tree
 * [aspect docs](aspect_docs.md)	 - Open documentation in the browser
 * [aspect info](aspect_info.md)	 - Displays runtime info about the bazel server
 * [aspect version](aspect_version.md)	 - Print the version of aspect CLI as well as tools it invokes
diff --git a/docs/aspect_build.md b/docs/aspect_build.md
index 7dd414d..5a8b18a 100644
--- a/docs/aspect_build.md
+++ b/docs/aspect_build.md
@@ -1,6 +1,6 @@
 ## aspect build
 
-Builds the specified targets, using the options.
+Builds the specified targets, using the options
 
 ### Synopsis
 
diff --git a/docs/aspect_clean.md b/docs/aspect_clean.md
new file mode 100644
index 0000000..3f40876
--- /dev/null
+++ b/docs/aspect_clean.md
@@ -0,0 +1,68 @@
+## aspect clean
+
+Removes the output tree
+
+### Synopsis
+
+Removes bazel-created output, including all object files, and bazel metadata.
+
+clean deletes the output directories for all build configurations performed by
+this Bazel instance, or the entire working tree created by this Bazel instance,
+and resets internal caches.
+
+If executed without any command-line options, then the output directory for all
+configurations will be cleaned.
+
+Recall that each Bazel instance is associated with a single workspace,
+thus the clean command will delete all outputs from all builds you've
+done with that Bazel instance in that workspace.
+
+NOTE: clean is primarily intended for reclaiming disk space for workspaces
+that are no longer needed.
+It causes all subsequent builds to be non-incremental.
+If this is not your intent, consider these alternatives:
+
+Do a one-off non-incremental build:
+	bazel --output_base=$(mktemp -d) ...
+
+Force repository rules to re-execute:
+	bazel sync --configure
+
+Workaround inconistent state:
+	Bazel's incremental rebuilds are designed to be correct, so clean
+	should never be required due to inconsistencies in the build.
+	Such problems are fixable and these bugs are a high priority.
+	If you ever find an incorrect incremental build, please file a bug report,
+	and only use clean as a temporary workaround.
+
+```
+aspect clean [flags]
+```
+
+### Options
+
+```
+      --expunge         Remove the entire output_base tree.
+                        This removes all build output, external repositories,
+                        and temp files created by Bazel.
+                        It also stops the Bazel server after the clean,
+                        equivalent to the shutdown command.
+      --expunge_async   Expunge in the background.
+                        It is safe to invoke a Bazel command in the same
+                        workspace while the asynchronous expunge continues to run.
+                        Note, however, that this may introduce IO contention.
+  -h, --help            help for clean
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.aspect.yaml)
+      --interactive     Interactive mode (e.g. prompts for user input)
+```
+
+### SEE ALSO
+
+* [aspect](aspect.md)	 - Aspect.build bazel wrapper
+
+###### Auto generated by spf13/cobra
diff --git a/docs/aspect_docs.md b/docs/aspect_docs.md
index 8533a3e..d5a636d 100644
--- a/docs/aspect_docs.md
+++ b/docs/aspect_docs.md
@@ -5,8 +5,8 @@
 ### Synopsis
 
 Given a selected topic, open the relevant API docs in a browser window.
-		The mechanism of choosing the browser to open is documented at https://github.com/pkg/browser
-		By default, opens docs.bazel.build
+The mechanism of choosing the browser to open is documented at https://github.com/pkg/browser
+By default, opens docs.bazel.build
 
 ```
 aspect docs [flags]
diff --git a/pkg/aspect/clean/BUILD.bazel b/pkg/aspect/clean/BUILD.bazel
new file mode 100644
index 0000000..281fd6f
--- /dev/null
+++ b/pkg/aspect/clean/BUILD.bazel
@@ -0,0 +1,27 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "clean",
+    srcs = ["clean.go"],
+    importpath = "aspect.build/cli/pkg/aspect/clean",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/aspecterrors",
+        "//pkg/bazel",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
+
+go_test(
+    name = "clean_test",
+    srcs = ["clean_test.go"],
+    deps = [
+        ":clean",
+        "//pkg/aspecterrors",
+        "//pkg/bazel/mock",
+        "//pkg/ioutils",
+        "@com_github_golang_mock//gomock",
+        "@com_github_onsi_gomega//:gomega",
+    ],
+)
diff --git a/pkg/aspect/clean/clean.go b/pkg/aspect/clean/clean.go
new file mode 100644
index 0000000..7450630
--- /dev/null
+++ b/pkg/aspect/clean/clean.go
@@ -0,0 +1,64 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package clean
+
+import (
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/pkg/aspecterrors"
+	"aspect.build/cli/pkg/bazel"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+// Clean represents the aspect clean command.
+type Clean struct {
+	ioutils.Streams
+	bzl bazel.Spawner
+
+	Expunge      bool
+	ExpungeAsync bool
+}
+
+// New creates a Clean command.
+func New(
+	streams ioutils.Streams,
+	bzl bazel.Spawner,
+) *Clean {
+	return &Clean{
+		Streams: streams,
+		bzl:     bzl,
+	}
+}
+
+// Run runs the aspect build command.
+func (c *Clean) Run(_ *cobra.Command, _ []string) error {
+	// TODO(alex): when interactive, prompt the user:
+	// First time running aspect clean?
+	// Then ask, why do you want to clean?
+	// - reclaim disk space?
+	// - workaround inconsistent state
+	// - experiment with a one-off non-incremental build
+	// then ask
+	// do you want to see this wizard again next time?
+	// and if not, record in the cache file to inhibit next time
+	cmd := []string{"clean"}
+	if c.Expunge {
+		cmd = append(cmd, "--expunge")
+	}
+	if c.ExpungeAsync {
+		cmd = append(cmd, "--expunge_async")
+	}
+	if exitCode, err := c.bzl.Spawn(cmd); exitCode != 0 {
+		err = &aspecterrors.ExitError{
+			Err:      err,
+			ExitCode: exitCode,
+		}
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/aspect/clean/clean_test.go b/pkg/aspect/clean/clean_test.go
new file mode 100644
index 0000000..51a9860
--- /dev/null
+++ b/pkg/aspect/clean/clean_test.go
@@ -0,0 +1,74 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package clean_test
+
+import (
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	. "github.com/onsi/gomega"
+
+	"aspect.build/cli/pkg/aspect/clean"
+	"aspect.build/cli/pkg/bazel/mock"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+func TestClean(t *testing.T) {
+
+	t.Run("clean calls bazel clean", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		spawner := mock.NewMockSpawner(ctrl)
+		spawner.
+			EXPECT().
+			Spawn([]string{"clean"}).
+			Return(0, nil)
+
+		b := clean.New(ioutils.Streams{}, spawner)
+		err := b.Run(nil, []string{})
+
+		g.Expect(err).To(BeNil())
+	})
+
+	t.Run("clean expunge calls bazel clean expunge", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		spawner := mock.NewMockSpawner(ctrl)
+		spawner.
+			EXPECT().
+			Spawn([]string{"clean", "--expunge"}).
+			Return(0, nil)
+
+		b := clean.New(ioutils.Streams{}, spawner)
+		b.Expunge = true
+		err := b.Run(nil, []string{})
+
+		g.Expect(err).To(BeNil())
+	})
+
+	t.Run("clean expunge_async calls bazel clean expunge_async", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		spawner := mock.NewMockSpawner(ctrl)
+		spawner.
+			EXPECT().
+			Spawn([]string{"clean", "--expunge_async"}).
+			Return(0, nil)
+
+		b := clean.New(ioutils.Streams{}, spawner)
+		b.ExpungeAsync = true
+		err := b.Run(nil, []string{})
+
+		g.Expect(err).To(BeNil())
+	})
+}