diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -165,7 +165,7 @@
     'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase.php',
     'ArcanistDefaultParametersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php',
     'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDefaultParametersXHPASTLinterRuleTestCase.php',
-    'ArcanistDefaultUnitFormatter' => 'unit/formatter/ArcanistDefaultUnitFormatter.php',
+    'ArcanistDefaultUnitSink' => 'unit/sink/ArcanistDefaultUnitSink.php',
     'ArcanistDefaultsConfigurationSource' => 'config/source/ArcanistDefaultsConfigurationSource.php',
     'ArcanistDeprecationXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeprecationXHPASTLinterRule.php',
     'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeprecationXHPASTLinterRuleTestCase.php',
@@ -279,7 +279,7 @@
     'ArcanistJSONLintRenderer' => 'lint/renderer/ArcanistJSONLintRenderer.php',
     'ArcanistJSONLinter' => 'lint/linter/ArcanistJSONLinter.php',
     'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php',
-    'ArcanistJSONUnitFormatter' => 'unit/formatter/ArcanistJSONUnitFormatter.php',
+    'ArcanistJSONUnitSink' => 'unit/sink/ArcanistJSONUnitSink.php',
     'ArcanistJscsLinter' => 'lint/linter/ArcanistJscsLinter.php',
     'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php',
     'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php',
@@ -473,9 +473,9 @@
     'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase.php',
     'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php',
     'ArcanistUnitEngine' => 'unit/engine/ArcanistUnitEngine.php',
-    'ArcanistUnitFormatter' => 'unit/formatter/ArcanistUnitFormatter.php',
     'ArcanistUnitOverseer' => 'unit/overseer/ArcanistUnitOverseer.php',
     'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php',
+    'ArcanistUnitSink' => 'unit/sink/ArcanistUnitSink.php',
     'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php',
     'ArcanistUnitTestResultTestCase' => 'unit/__tests__/ArcanistUnitTestResultTestCase.php',
     'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php',
@@ -1276,7 +1276,7 @@
     'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
     'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
     'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
-    'ArcanistDefaultUnitFormatter' => 'ArcanistUnitFormatter',
+    'ArcanistDefaultUnitSink' => 'ArcanistUnitSink',
     'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource',
     'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
     'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@@ -1390,7 +1390,7 @@
     'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer',
     'ArcanistJSONLinter' => 'ArcanistLinter',
     'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase',
-    'ArcanistJSONUnitFormatter' => 'ArcanistUnitFormatter',
+    'ArcanistJSONUnitSink' => 'ArcanistUnitSink',
     'ArcanistJscsLinter' => 'ArcanistExternalLinter',
     'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase',
     'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@@ -1584,9 +1584,9 @@
     'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
     'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer',
     'ArcanistUnitEngine' => 'Phobject',
-    'ArcanistUnitFormatter' => 'Phobject',
     'ArcanistUnitOverseer' => 'Phobject',
     'ArcanistUnitRenderer' => 'Phobject',
+    'ArcanistUnitSink' => 'Phobject',
     'ArcanistUnitTestResult' => 'Phobject',
     'ArcanistUnitTestResultTestCase' => 'PhutilTestCase',
     'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine',
diff --git a/src/unit/ArcanistUnitTestResult.php b/src/unit/ArcanistUnitTestResult.php
--- a/src/unit/ArcanistUnitTestResult.php
+++ b/src/unit/ArcanistUnitTestResult.php
@@ -175,13 +175,8 @@
   }
 
   public static function getAllResultCodes() {
-    return array(
-      self::RESULT_PASS,
-      self::RESULT_FAIL,
-      self::RESULT_SKIP,
-      self::RESULT_BROKEN,
-      self::RESULT_UNSOUND,
-    );
+    $map = self::getResultCodeSpecs();
+    return array_keys($map);
   }
 
   public static function getResultCodeName($result_code) {
@@ -205,31 +200,56 @@
     return idx($specs, $result_code);
   }
 
+  public function getANSIColor() {
+    $spec = $this->getResultMap();
+    return idx($spec, 'color.ansi', 'red');
+  }
+
+  public function getResultLabel() {
+    $spec = $this->getResultMap();
+    return idx($spec, 'label', $this->getResult());
+  }
+
+  private function getResultMap() {
+    $map = self::getResultCodeSpecs();
+    return idx($map, $this->getResult(), array());
+  }
+
   private static function getResultCodeSpecs() {
     return array(
       self::RESULT_PASS => array(
         'name' => pht('Pass'),
+        'label' => pht('PASS'),
+        'color.ansi' => 'green',
         'description' => pht(
           'The test passed.'),
       ),
       self::RESULT_FAIL => array(
         'name' => pht('Fail'),
+        'label' => pht('FAIL'),
+        'color.ansi' => 'red',
         'description' => pht(
           'The test failed.'),
       ),
       self::RESULT_SKIP => array(
         'name' => pht('Skip'),
+        'label' => pht('SKIP'),
+        'color.ansi' => 'cyan',
         'description' => pht(
           'The test was not executed.'),
       ),
       self::RESULT_BROKEN => array(
         'name' => pht('Broken'),
+        'label' => pht('BROKEN'),
+        'color.ansi' => 'red',
         'description' => pht(
           'The test failed in an abnormal or severe way. For example, the '.
           'harness crashed instead of reporting a failure.'),
       ),
       self::RESULT_UNSOUND => array(
         'name' => pht('Unsound'),
+        'label' => pht('UNSOUND'),
+        'color.ansi' => 'yellow',
         'description' => pht(
           'The test failed, but this change is probably not what broke it. '.
           'For example, it might have already been failing.'),
@@ -237,5 +257,15 @@
     );
   }
 
+  public function getDisplayName() {
+    $name = $this->getName();
+
+    $namespace = $this->getNamespace();
+    if (strlen($namespace)) {
+      $name = $namespace.'::'.$name;
+    }
+
+    return $name;
+  }
 
 }
diff --git a/src/unit/engine/ArcanistUnitEngine.php b/src/unit/engine/ArcanistUnitEngine.php
--- a/src/unit/engine/ArcanistUnitEngine.php
+++ b/src/unit/engine/ArcanistUnitEngine.php
@@ -56,14 +56,7 @@
   abstract public function runTests();
 
   final protected function didRunTests(array $tests) {
-    assert_instances_of($tests, 'ArcanistUnitTestResult');
-
-    // TOOLSETS: Pass this stuff to result output so it can print progress or
-    // stream results.
-
-    foreach ($tests as $test) {
-      echo "Ran Test: ".$test->getNamespace().'::'.$test->getName()."\n";
-    }
+    return $this->getOverseer()->didRunTests($tests);
   }
 
 }
\ No newline at end of file
diff --git a/src/unit/formatter/ArcanistDefaultUnitFormatter.php b/src/unit/formatter/ArcanistDefaultUnitFormatter.php
deleted file mode 100644
--- a/src/unit/formatter/ArcanistDefaultUnitFormatter.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-final class ArcanistDefaultUnitFormatter
-  extends ArcanistUnitFormatter {
-
-  const FORMATTER_KEY = 'default';
-
-
-}
diff --git a/src/unit/formatter/ArcanistJSONUnitFormatter.php b/src/unit/formatter/ArcanistJSONUnitFormatter.php
deleted file mode 100644
--- a/src/unit/formatter/ArcanistJSONUnitFormatter.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-final class ArcanistJSONUnitFormatter
-  extends ArcanistUnitFormatter {
-
-  const FORMATTER_KEY = 'json';
-
-
-}
diff --git a/src/unit/formatter/ArcanistUnitFormatter.php b/src/unit/formatter/ArcanistUnitFormatter.php
deleted file mode 100644
--- a/src/unit/formatter/ArcanistUnitFormatter.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-abstract class ArcanistUnitFormatter
-  extends Phobject {
-
-  final public function getUnitFormatterKey() {
-    return $this->getPhobjectClassConstant('FORMATTER_KEY');
-  }
-
-  public static function getAllUnitFormatters() {
-    return id(new PhutilClassMapQuery())
-      ->setAncestorClass(__CLASS__)
-      ->setUniqueMethod('getUnitFormatterKey')
-      ->execute();
-  }
-
-}
diff --git a/src/unit/overseer/ArcanistUnitOverseer.php b/src/unit/overseer/ArcanistUnitOverseer.php
--- a/src/unit/overseer/ArcanistUnitOverseer.php
+++ b/src/unit/overseer/ArcanistUnitOverseer.php
@@ -5,9 +5,9 @@
 
   private $directory;
   private $paths = array();
-  private $formatter;
+  private $sinks = array();
 
-  public function setPaths($paths) {
+  public function setPaths(array $paths) {
     $this->paths = $paths;
     return $this;
   }
@@ -16,13 +16,14 @@
     return $this->paths;
   }
 
-  public function setFormatter(ArcanistUnitFormatter $formatter) {
-    $this->formatter = $formatter;
+  public function setSinks(array $sinks) {
+    assert_instances_of($sinks, 'ArcanistUnitSink');
+    $this->sinks = $sinks;
     return $this;
   }
 
-  public function getFormatter() {
-    return $this->formatter;
+  public function getSinks() {
+    return $this->sinks;
   }
 
   public function setDirectory($directory) {
@@ -50,9 +51,27 @@
       }
     }
 
+    $this->didCompleteTests($results);
+
     return $results;
   }
 
+  public function didRunTests(array $tests) {
+    assert_instances_of($tests, 'ArcanistUnitTestResult');
+
+    foreach ($this->getSinks() as $sink) {
+      $sink->sinkPartialResults($tests);
+    }
+  }
+
+  private function didCompleteTests(array $tests) {
+    assert_instances_of($tests, 'ArcanistUnitTestResult');
+
+    foreach ($this->getSinks() as $sink) {
+      $sink->sinkFinalResults($tests);
+    }
+  }
+
   private function loadEngines() {
     $root = $this->getDirectory();
 
diff --git a/src/unit/sink/ArcanistDefaultUnitSink.php b/src/unit/sink/ArcanistDefaultUnitSink.php
new file mode 100644
--- /dev/null
+++ b/src/unit/sink/ArcanistDefaultUnitSink.php
@@ -0,0 +1,169 @@
+<?php
+
+final class ArcanistDefaultUnitSink
+  extends ArcanistUnitSink {
+
+  const SINKKEY = 'default';
+
+  private $lastUpdateTime;
+  private $buffer = array();
+
+  public function sinkPartialResults(array $results) {
+
+    // We want to show the user both regular progress reports and make sure
+    // that important results aren't scrolled off screen. We'll print a summary
+    // at the end so it's not critical that users can never miss important
+    // results, but they may (for example) want to ^C early if tests fail and
+    // their fate is sealed.
+
+
+    $failed = array();
+    foreach ($results as $result) {
+      $result_code = $result->getResult();
+      switch ($result_code) {
+        case ArcanistUnitTestResult::RESULT_PASS:
+        case ArcanistUnitTestResult::RESULT_SKIP:
+          break;
+        default:
+          $failed[] = $result;
+          break;
+      }
+    }
+
+    $now = microtime(true);
+
+    if (!$failed) {
+      if ($this->lastUpdateTime) {
+        $delay = 1;
+        if (($now - $this->lastUpdateTime) < $delay) {
+          $this->buffer[] = $results;
+          return;
+        }
+      }
+    }
+
+    $this->buffer[] = $results;
+    $results = array_mergev($this->buffer);
+    $this->buffer = array();
+
+    $pass_count = 0;
+    $skip_count = 0;
+    $failed = array();
+    foreach ($results as $result) {
+      $result_code = $result->getResult();
+      switch ($result_code) {
+        case ArcanistUnitTestResult::RESULT_PASS:
+          $pass_count++;
+          break;
+        case ArcanistUnitTestResult::RESULT_SKIP:
+          $skip_count++;
+          break;
+        default:
+          $failed[] = $result;
+          break;
+      }
+    }
+
+    if ($pass_count) {
+      echo tsprintf(
+        "%s\n",
+        pht('%s tests passed.', $pass_count));
+    }
+
+    if ($skip_count) {
+      echo tsprintf(
+        "%s\n",
+        pht('%s tests skipped.', $skip_count));
+    }
+
+    foreach ($failed as $result) {
+      echo $this->getDisplayForTest($result, false);
+    }
+
+    $this->lastUpdateTime = $now;
+
+    return $this;
+  }
+
+  public function sinkFinalResults(array $results) {
+
+    $passed = array();
+    $skipped = array();
+    $failed = array();
+    foreach ($results as $result) {
+      $result_code = $result->getResult();
+      switch ($result_code) {
+        case ArcanistUnitTestResult::RESULT_PASS:
+          $passed[] = $result;
+          break;
+        case ArcanistUnitTestResult::RESULT_SKIP:
+          $skipped[] = $result;
+          break;
+        default:
+          $failed[] = $result;
+          break;
+      }
+    }
+
+    echo tsprintf(
+      "%s\n",
+      pht('RESULT SUMMARY'));
+
+
+    if ($skipped) {
+      echo tsprintf(
+        "%s\n",
+        pht('SKIPPED TESTS'));
+
+      foreach ($skipped as $result) {
+        echo $this->getDisplayForTest($result);
+      }
+    }
+
+    if ($failed) {
+      echo tsprintf(
+        "%s\n",
+        pht('FAILED TESTS'));
+
+      foreach ($failed as $result) {
+        echo $this->getDisplayForTest($result);
+      }
+    }
+
+
+    echo tsprintf(
+      "**<bg:red> ~~~ %s </bg>**\n",
+      pht(
+        "%s PASSED * %s SKIPPED * %s FAILED/BROKEN/UNSTABLE",
+        phutil_count($passed),
+        phutil_count($skipped),
+        phutil_count($failed)));
+  }
+
+  private function getDisplayForTest(ArcanistUnitTestResult $result) {
+
+    $color = $result->getANSIColor();
+    $status = $result->getResultLabel();
+    $name = $result->getDisplayName();
+
+    // TOOLSETS: Restore timing information.
+    $timing = '    ';
+
+    $output = tsprintf(
+      "**<bg:".$color."> %s </bg>** %s %s\n",
+      $status,
+      $timing,
+      $name);
+
+    $user_data = $result->getUserData();
+    if (strlen($user_data)) {
+      $output = tsprintf(
+        "%s%B\n",
+        $output,
+        $user_data);
+    }
+
+    return $output;
+  }
+
+}
diff --git a/src/unit/sink/ArcanistJSONUnitSink.php b/src/unit/sink/ArcanistJSONUnitSink.php
new file mode 100644
--- /dev/null
+++ b/src/unit/sink/ArcanistJSONUnitSink.php
@@ -0,0 +1,9 @@
+<?php
+
+final class ArcanistJSONUnitSink
+  extends ArcanistUnitSink {
+
+  const SINKKEY = 'json';
+
+
+}
diff --git a/src/unit/sink/ArcanistUnitSink.php b/src/unit/sink/ArcanistUnitSink.php
new file mode 100644
--- /dev/null
+++ b/src/unit/sink/ArcanistUnitSink.php
@@ -0,0 +1,31 @@
+<?php
+
+abstract class ArcanistUnitSink
+  extends Phobject {
+
+  private $results;
+
+  final public function getUnitSinkKey() {
+    return $this->getPhobjectClassConstant('SINKKEY');
+  }
+
+  public static function getAllUnitSinks() {
+    return id(new PhutilClassMapQuery())
+      ->setAncestorClass(__CLASS__)
+      ->setUniqueMethod('getUnitSinkKey')
+      ->execute();
+  }
+
+  public function sinkPartialResults(array $results) {
+    return $this;
+  }
+
+  public function sinkFinalResults(array $results) {
+    return $this;
+  }
+
+  public function getOutput() {
+    return null;
+  }
+
+}
diff --git a/src/workflow/ArcanistUnitWorkflow.php b/src/workflow/ArcanistUnitWorkflow.php
--- a/src/workflow/ArcanistUnitWorkflow.php
+++ b/src/workflow/ArcanistUnitWorkflow.php
@@ -23,7 +23,7 @@
     return array(
       $this->newWorkflowArgument('commit')
         ->setParameter('commit'),
-      $this->newWorkflowArgument('format')
+      $this->newWorkflowArgument('sink')
         ->setParameter('format'),
       $this->newWorkflowArgument('everything'),
       $this->newWorkflowArgument('paths')
@@ -51,32 +51,40 @@
     // though it is "arc unit --everything", and ignoring the "--commit" flag
     // and "paths" arguments.
 
-    $formatter = $this->newUnitFormatter();
-    $overseer->setFormatter($formatter);
+    $sinks = array();
+    $sinks[] = $this->newUnitSink();
+    $overseer->setSinks($sinks);
 
     $overseer->execute();
 
+    foreach ($sinks as $sink) {
+      $result = $sink->getOutput();
+      if ($result !== null) {
+        echo $result;
+      }
+    }
+
     return 0;
   }
 
-  private function newUnitFormatter() {
-    $formatters = ArcanistUnitFormatter::getAllUnitFormatters();
-    $format_key = $this->getArgument('format');
-    if (!strlen($format_key)) {
-      $format_key = ArcanistDefaultUnitFormatter::FORMATTER_KEY;
+  private function newUnitSink() {
+    $sinks = ArcanistUnitSink::getAllUnitSinks();
+    $sink_key = $this->getArgument('sink');
+    if (!strlen($sink_key)) {
+      $sink_key = ArcanistDefaultUnitSink::SINKKEY;
     }
 
-    $formatter = idx($formatters, $format_key);
-    if (!$formatter) {
+    $sink = idx($sinks, $sink_key);
+    if (!$sink) {
       throw new ArcanistUsageException(
         pht(
-          'Unit test output format ("%s") is unknown. Supported formats '.
+          'Unit test output sink ("%s") is unknown. Supported sinks '.
           'are: %s.',
-          $format_key,
-          implode(', ', array_keys($formatters))));
+          $sink_key,
+          implode(', ', array_keys($sinks))));
     }
 
-    return $formatter;
+    return $sink;
   }
 
 }