Index: src/__phutil_library_map__.php
===================================================================
--- src/__phutil_library_map__.php
+++ src/__phutil_library_map__.php
@@ -718,7 +718,6 @@
     'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php',
     'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php',
     'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php',
-    'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php',
     'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php',
     'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
     'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
@@ -1841,7 +1840,6 @@
     'PhutilExecPassthru' => 'PhutilExecutableFuture',
     'PhutilExecutableFuture' => 'Future',
     'PhutilExecutionEnvironment' => 'Phobject',
-    'PhutilExtensionsTestCase' => 'PhutilTestCase',
     'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilFileLock' => 'PhutilLock',
Index: src/toolset/ArcanistWorkflow.php
===================================================================
--- src/toolset/ArcanistWorkflow.php
+++ src/toolset/ArcanistWorkflow.php
@@ -129,9 +129,13 @@
   }
 
   final public function executeWorkflow(PhutilArgumentParser $args) {
+    $runtime = $this->getRuntime();
+
     $this->arguments = $args;
     $caught = null;
 
+    $runtime->pushWorkflow($this);
+
     try {
       $err = $this->runWorkflow($args);
     } catch (Exception $ex) {
@@ -144,6 +148,8 @@
       phlog($ex);
     }
 
+    $runtime->popWorkflow();
+
     if ($caught) {
       throw $caught;
     }
@@ -189,4 +195,12 @@
     return $this->getRuntime()->getLogEngine();
   }
 
+  public function canHandleInterrupt() {
+    return false;
+  }
+
+  public function handleInterrupt() {
+    throw new PhutilMethodNotImplementedException();
+  }
+
 }
Index: src/workflow/ArcanistWeldWorkflow.php
===================================================================
--- src/workflow/ArcanistWeldWorkflow.php
+++ src/workflow/ArcanistWeldWorkflow.php
@@ -1,36 +1,38 @@
 <?php
 
-final class ArcanistWeldWorkflow extends ArcanistWorkflow {
+final class ArcanistWeldWorkflow
+  extends ArcanistWorkflow {
 
   public function getWorkflowName() {
     return 'weld';
   }
 
-  public function getCommandSynopses() {
-    return phutil_console_format(<<<EOTEXT
-      **weld** [options] __file__ __file__ ...
+  public function getWorkflowInformation() {
+    $help = pht(<<<EOTEXT
+Robustly fuse two or more files together. The resulting joint is much stronger
+than the one created by tools like __cat__.
 EOTEXT
-      );
-  }
+);
 
-  public function getCommandHelp() {
-    return phutil_console_format(<<<EOTEXT
-          Robustly fuse two or more files together. The resulting joint is
-          much stronger than the one created by tools like __cat__.
-EOTEXT
-      );
+    return $this->newWorkflowInformation()
+      ->addExample(pht('**weld** [__options__] __file__ __file__ ...'))
+      ->setHelp($help);
   }
 
-  public function getArguments() {
+  public function getWorkflowArguments() {
     return array(
-      '*' => 'files',
+      $this->newWorkflowArgument('files')
+        ->setIsPathArgument(true)
+        ->setWildcard(true),
     );
   }
 
-  public function run() {
+
+  public function runWorkflow() {
     $files = $this->getArgument('files');
+
     if (count($files) < 2) {
-      throw new ArcanistUsageException(
+      throw new PhutilArgumentUsageException(
         pht('Specify two or more files to weld together.'));
     }
 
Index: support/ArcanistRuntime.php
===================================================================
--- support/ArcanistRuntime.php
+++ support/ArcanistRuntime.php
@@ -4,6 +4,9 @@
 
   private $workflows;
   private $logEngine;
+  private $lastInterruptTime;
+
+  private $stack = array();
 
   public function execute(array $argv) {
 
@@ -68,6 +71,12 @@
 
     $log->writeTrace(pht('ARGV'), csprintf('%Ls', $argv));
 
+    // We're installing the signal handler after parsing "--trace" so that it
+    // can emit debugging messages. This means there's a very small window at
+    // startup where signals have no special handling, but we couldn't really
+    // route them or do anything interesting with them anyway.
+    $this->installSignalHandler();
+
     $args->parsePartial($config_args, true);
 
     $config_engine = $this->loadConfiguration($args);
@@ -514,4 +523,103 @@
     return $argv;
   }
 
+  private function installSignalHandler() {
+    $log = $this->getLogEngine();
+
+    if (!function_exists('pcntl_signal')) {
+      $log->writeTrace(
+        pht('PCNTL'),
+        pht(
+          'Unable to install signal handler, pcntl_signal() unavailable. '.
+          'Continuing without signal handling.'));
+      return;
+    }
+
+    // NOTE: SIGHUP, SIGTERM and SIGWINCH are handled by "PhutilSignalRouter".
+    // This logic is largely similar to the logic there, but more specific to
+    // Arcanist workflows.
+
+    pcntl_signal(SIGINT, array($this, 'routeSignal'));
+  }
+
+  public function routeSignal($signo) {
+    switch ($signo) {
+      case SIGINT:
+        $this->routeInterruptSignal($signo);
+        break;
+    }
+  }
+
+  private function routeInterruptSignal($signo) {
+    $log = $this->getLogEngine();
+
+    $last_interrupt = $this->lastInterruptTime;
+    $now = microtime(true);
+    $this->lastInterruptTime = $now;
+
+    $should_exit = false;
+
+    // If we received another SIGINT recently, always exit. This implements
+    // "press ^C twice in quick succession to exit" regardless of what the
+    // workflow may decide to do.
+    $interval = 2;
+    if ($last_interrupt !== null) {
+      if ($now - $last_interrupt < $interval) {
+        $should_exit = true;
+      }
+    }
+
+    $handler = null;
+    if (!$should_exit) {
+
+      // Look for an interrupt handler in the current workflow stack.
+
+      $stack = $this->getWorkflowStack();
+      foreach ($stack as $workflow) {
+        if ($workflow->canHandleInterrupt()) {
+          $handler = $workflow;
+          break;
+        }
+      }
+
+      // If no workflow in the current execution stack can handle an interrupt
+      // signal, just exit on the first interrupt.
+
+      if (!$handler) {
+        $should_exit = true;
+      }
+    }
+
+    if ($should_exit) {
+      $log->writeHint(
+        pht('INTERRUPT'),
+        pht('Interrupted by SIGINT (^C).'));
+      exit(128 + $signo);
+    }
+
+    $log->writeHint(
+      pht('INTERRUPT'),
+      pht('Press ^C again to exit.'));
+
+    $handler->handleInterrupt();
+  }
+
+  public function pushWorkflow(ArcanistWorkflow $workflow) {
+    $this->stack[] = $workflow;
+    return $this;
+  }
+
+  public function popWorkflow() {
+    if (!$this->stack) {
+      throw new Exception(pht('Trying to pop an empty workflow stack!'));
+    }
+
+    return array_pop($this->stack);
+  }
+
+  public function getWorkflowStack() {
+    return $this->stack;
+  }
+
+
 }