diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php
index 922a62e..d51155a 100644
--- a/src/__phutil_library_init__.php
+++ b/src/__phutil_library_init__.php
@@ -1,334 +1,317 @@
 <?php
 
 define('__LIBPHUTIL__', true);
 
 /**
  * @group library
  */
 function phutil_register_library($library, $path) {
   PhutilBootloader::getInstance()->registerLibrary($library, $path);
 }
 
 /**
  * @group library
  */
 function phutil_register_library_map(array $map) {
   PhutilBootloader::getInstance()->registerLibraryMap($map);
 }
 
 /**
  * @group library
  */
 function phutil_load_library($path) {
   PhutilBootloader::getInstance()->loadLibrary($path);
 }
 
-/**
- * @group library
- */
-function phutil_is_windows() {
-  // We can also use PHP_OS, but that's kind of sketchy because it returns
-  // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for
-  // DIRECTORY_SEPARATOR is more straightforward.
-  return (DIRECTORY_SEPARATOR != '/');
-}
-
-/**
- * @group library
- */
-function phutil_is_hiphop_runtime() {
-  return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1);
-}
-
 /**
  * @group library
  */
 final class PhutilBootloader {
 
   private static $instance;
 
   private $registeredLibraries  = array();
   private $libraryMaps          = array();
   private $currentLibrary       = null;
   private $classTree            = array();
 
   public static function getInstance() {
     if (!self::$instance) {
       self::$instance = new PhutilBootloader();
     }
     return self::$instance;
   }
 
   private function __construct() {
     // This method intentionally left blank.
   }
 
   public function getClassTree() {
     return $this->classTree;
   }
 
   public function registerLibrary($name, $path) {
     if (basename($path) != '__phutil_library_init__.php') {
       throw new PhutilBootloaderException(
         'Only directories with a __phutil_library_init__.php file may be '.
         'registered as libphutil libraries.');
     }
 
     $path = dirname($path);
 
     // Detect attempts to load the same library multiple times from different
     // locations. This might mean you're doing something silly like trying to
     // include two different versions of something, or it might mean you're
     // doing something subtle like running a different version of 'arc' on a
     // working copy of Arcanist.
     if (isset($this->registeredLibraries[$name])) {
       $old_path = $this->registeredLibraries[$name];
       if ($old_path != $path) {
         throw new PhutilLibraryConflictException($name, $old_path, $path);
       }
     }
 
     $this->registeredLibraries[$name] = $path;
 
     // For libphutil v2 libraries, load all functions when we load the library.
 
     if (!class_exists('PhutilSymbolLoader', false)) {
       $root = $this->getLibraryRoot('phutil');
       $this->executeInclude($root.'/symbols/PhutilSymbolLoader.php');
     }
 
     $loader = new PhutilSymbolLoader();
     $loader
       ->setLibrary($name)
       ->setType('function');
 
     try {
       $loader->selectAndLoadSymbols();
     } catch (PhutilBootloaderException $ex) {
       // Ignore this, it happens if a global function's file is removed or
       // similar. Worst case is that we fatal when calling the function, which
       // is no worse than fataling here.
     } catch (PhutilMissingSymbolException $ex) {
       // Ignore this, it happens if a global function is removed. Everything
       // else loaded so proceed forward: worst case is a fatal when we
       // hit a function call to a function which no longer exists, which is
       // no worse than fataling here.
     }
 
     return $this;
   }
 
   public function registerLibraryMap(array $map) {
     $this->libraryMaps[$this->currentLibrary] = $map;
     return $this;
   }
 
   public function getLibraryMap($name) {
     if (empty($this->libraryMaps[$name])) {
       $root = $this->getLibraryRoot($name);
       $this->currentLibrary = $name;
       $okay = include $root.'/__phutil_library_map__.php';
       if (!$okay) {
         throw new PhutilBootloaderException(
           "Include of '{$root}/__phutil_library_map__.php' failed!");
       }
 
       $map = $this->libraryMaps[$name];
 
       // NOTE: We can't use "idx()" here because it may not be loaded yet.
       $version = isset($map['__library_version__'])
         ? $map['__library_version__']
         : 1;
 
       switch ($version) {
         case 1:
           throw new Exception(
             "libphutil v1 libraries are no longer supported.");
         case 2:
           // NOTE: In version 2 of the library format, all parents (both
           // classes and interfaces) are stored in the 'xmap'. The value is
           // either a string for a single parent (the common case) or an array
           // for multiple parents.
           foreach ($map['xmap'] as $child => $parents) {
             foreach ((array)$parents as $parent) {
               $this->classTree[$parent][] = $child;
             }
           }
           break;
         default:
           throw new Exception("Unsupported library version '{$version}'!");
       }
     }
     return $this->libraryMaps[$name];
   }
 
   public function getLibraryRoot($name) {
     if (empty($this->registeredLibraries[$name])) {
       throw new PhutilBootloaderException(
         "The phutil library '{$name}' has not been loaded!");
     }
     return $this->registeredLibraries[$name];
   }
 
   public function getAllLibraries() {
     return array_keys($this->registeredLibraries);
   }
 
   public function loadLibrary($path) {
     $root = null;
     if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
       if ($path[0] != '/') {
         $root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
       }
     }
     $okay = $this->executeInclude($root.$path.'/__phutil_library_init__.php');
     if (!$okay) {
       throw new PhutilBootloaderException(
         "Include of '{$path}/__phutil_library_init__.php' failed!");
     }
   }
 
   public function loadLibrarySource($library, $source) {
     $path = $this->getLibraryRoot($library).'/'.$source;
     $okay = $this->executeInclude($path);
     if (!$okay) {
       throw new PhutilBootloaderException("Include of '{$path}' failed!");
     }
   }
 
   private function executeInclude($path) {
     // Suppress warning spew if the file does not exist; we'll throw an
     // exception instead. We still emit error text in the case of syntax errors.
     $old = error_reporting(E_ALL & ~E_WARNING);
     $okay = include_once $path;
     error_reporting($old);
 
     return $okay;
   }
 
 }
 
 /**
  * @group library
  */
 final class PhutilBootloaderException extends Exception { }
 
 
 /**
  * Thrown when you attempt to load two different copies of a library with the
  * same name. Trying to load the second copy of the library will trigger this,
  * and the library will not be loaded.
  *
  * This means you've either done something silly (like tried to explicitly load
  * two different versions of the same library into the same program -- this
  * won't work because they'll have namespace conflicts), or your configuration
  * might have some problems which caused two parts of your program to try to
  * load the same library but end up loading different copies of it, or there
  * may be some subtle issue like running 'arc' in a different Arcanist working
  * directory. (Some bootstrapping workflows like that which run low-level
  * library components on other copies of themselves are expected to fail.)
  *
  * To resolve this, you need to make sure your program loads no more than one
  * copy of each libphutil library, but exactly how you approach this depends on
  * why it's happening in the first place.
  *
  * @task info Getting Exception Information
  * @task construct Creating Library Conflict Exceptions
  * @group library
  */
 final class PhutilLibraryConflictException extends Exception {
 
   private $library;
   private $oldPath;
   private $newPath;
 
   /**
    * Create a new library conflict exception.
    *
    * @param string The name of the library which conflicts with an existing
    *               library.
    * @param string The path of the already-loaded library.
    * @param string The path of the attempting-to-load library.
    *
    * @task construct
    */
   public function __construct($library, $old_path, $new_path) {
     $this->library = $library;
     $this->oldPath = $old_path;
     $this->newPath = $new_path;
 
     $message = "Library conflict! The library '{$library}' has already been ".
                "loaded (from '{$old_path}') but is now being loaded again ".
                "from a new location ('{$new_path}'). You can not load ".
                "multiple copies of the same library into a program.";
 
     parent::__construct($message);
   }
 
   /**
    * Retrieve the name of the library in conflict.
    *
    * @return string The name of the library which conflicts with an existing
    *                library.
    * @task info
    */
   public function getLibrary() {
     return $this->library;
   }
 
 
   /**
    * Get the path to the library which has already been loaded earlier in the
    * program's execution.
    *
    * @return string The path of the already-loaded library.
    * @task info
    */
   public function getOldPath() {
     return $this->oldPath;
   }
 
   /**
    * Get the path to the library which is causing this conflict.
    *
    * @return string The path of the attempting-to-load library.
    * @task info
    */
   public function getNewPath() {
     return $this->newPath;
   }
 }
 
 
 /**
  * @group library
  */
 function __phutil_autoload($class_name) {
   try {
     $loader = new PhutilSymbolLoader();
     $symbols = $loader
       ->setType('class')
       ->setName($class_name)
       ->selectAndLoadSymbols();
     if (!$symbols) {
       throw new PhutilMissingSymbolException(
         $class_name,
         'class or interface',
         "the class or interface '{$class_name}' is not defined in the library ".
         "map for any loaded phutil library.");
     }
   } catch (PhutilMissingSymbolException $ex) {
     // If there are other SPL autoloaders installed, we need to give them a
     // chance to load the class. Throw the exception if we're the last
     // autoloader; if not, swallow it and let them take a shot.
     $autoloaders = spl_autoload_functions();
     $last = end($autoloaders);
     if ($last == '__phutil_autoload') {
       throw $ex;
     }
   }
 }
 
 spl_autoload_register('__phutil_autoload', $throw = true);
 
 phutil_register_library('phutil', __FILE__);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index aab94d1..c3cd3e3 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,512 +1,520 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'arc liberate' to rebuild it.
  * @generated
  * @phutil-library-version 2
  */
 
 phutil_register_library_map(array(
   '__library_version__' => 2,
   'class' =>
   array(
     'AASTNode' => 'parser/aast/api/AASTNode.php',
     'AASTNodeList' => 'parser/aast/api/AASTNodeList.php',
     'AASTToken' => 'parser/aast/api/AASTToken.php',
     'AASTTree' => 'parser/aast/api/AASTTree.php',
     'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php',
     'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php',
     'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php',
     'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php',
     'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php',
     'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
     'AphrontMySQLDatabaseConnectionBase' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnectionBase.php',
     'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
     'AphrontQueryAccessDeniedException' => 'aphront/storage/exception/AphrontQueryAccessDeniedException.php',
     'AphrontQueryConnectionException' => 'aphront/storage/exception/AphrontQueryConnectionException.php',
     'AphrontQueryConnectionLostException' => 'aphront/storage/exception/AphrontQueryConnectionLostException.php',
     'AphrontQueryCountException' => 'aphront/storage/exception/AphrontQueryCountException.php',
     'AphrontQueryDeadlockException' => 'aphront/storage/exception/AphrontQueryDeadlockException.php',
     'AphrontQueryDuplicateKeyException' => 'aphront/storage/exception/AphrontQueryDuplicateKeyException.php',
     'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php',
     'AphrontQueryNotSupportedException' => 'aphront/storage/exception/AphrontQueryNotSupportedException.php',
     'AphrontQueryObjectMissingException' => 'aphront/storage/exception/AphrontQueryObjectMissingException.php',
     'AphrontQueryParameterException' => 'aphront/storage/exception/AphrontQueryParameterException.php',
     'AphrontQueryRecoverableException' => 'aphront/storage/exception/AphrontQueryRecoverableException.php',
     'AphrontQuerySchemaException' => 'aphront/storage/exception/AphrontQuerySchemaException.php',
     'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
     'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
     'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
     'CommandException' => 'future/exec/CommandException.php',
     'ConduitClient' => 'conduit/ConduitClient.php',
     'ConduitClientException' => 'conduit/ConduitClientException.php',
     'ConduitFuture' => 'conduit/ConduitFuture.php',
     'ExecFuture' => 'future/exec/ExecFuture.php',
     'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
     'FileFinder' => 'filesystem/FileFinder.php',
     'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
     'FileList' => 'filesystem/FileList.php',
     'Filesystem' => 'filesystem/Filesystem.php',
     'FilesystemException' => 'filesystem/FilesystemException.php',
     'Future' => 'future/Future.php',
     'FutureIterator' => 'future/FutureIterator.php',
     'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
     'FutureProxy' => 'future/FutureProxy.php',
     'HTTPFuture' => 'future/http/HTTPFuture.php',
     'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
     'HTTPFutureResponseStatusCURL' => 'future/http/status/HTTPFutureResponseStatusCURL.php',
     'HTTPFutureResponseStatusHTTP' => 'future/http/status/HTTPFutureResponseStatusHTTP.php',
     'HTTPFutureResponseStatusParse' => 'future/http/status/HTTPFutureResponseStatusParse.php',
     'HTTPFutureResponseStatusTransport' => 'future/http/status/HTTPFutureResponseStatusTransport.php',
     'HTTPSFuture' => 'future/http/HTTPSFuture.php',
     'ImmediateFuture' => 'future/ImmediateFuture.php',
     'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php',
     'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php',
     'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php',
     'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
     'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
     'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
+    'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php',
+    'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php',
+    'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php',
+    'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php',
     'Phobject' => 'object/Phobject.php',
     'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
     'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
     'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
     'PhutilAggregateException' => 'error/PhutilAggregateException.php',
     'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php',
     'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php',
     'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php',
     'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php',
     'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php',
     'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php',
     'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
     'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
     'PhutilArray' => 'utils/PhutilArray.php',
     'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
     'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
+    'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php',
     'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
     'PhutilBufferedIteratorExample' => 'utils/PhutilBufferedIteratorExample.php',
     'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
     'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
     'PhutilChannel' => 'channel/PhutilChannel.php',
     'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
     'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
     'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
     'PhutilConsole' => 'console/PhutilConsole.php',
     'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
     'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
     'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
     'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
     'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
     'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
     'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
     'PhutilDaemon' => 'daemon/PhutilDaemon.php',
     'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php',
     'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php',
     'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php',
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php',
     'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php',
     'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
     'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
     'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php',
     'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php',
     'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
     'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php',
     'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php',
     'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
     'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
     'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
     'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php',
     'PhutilEvent' => 'events/PhutilEvent.php',
     'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
     'PhutilEventEngine' => 'events/PhutilEventEngine.php',
     'PhutilEventListener' => 'events/PhutilEventListener.php',
     'PhutilEventType' => 'events/constant/PhutilEventType.php',
     'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php',
     'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
     'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
     'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
     'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
     'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
     'PhutilGitURI' => 'parser/PhutilGitURI.php',
     'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
     'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
     'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
     'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
     'PhutilJSON' => 'parser/PhutilJSON.php',
     'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
     'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
     'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
     'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php',
     'PhutilKeyValueCacheAPC' => 'cache/PhutilKeyValueCacheAPC.php',
     'PhutilKeyValueCacheDirectory' => 'cache/PhutilKeyValueCacheDirectory.php',
     'PhutilKeyValueCacheInRequest' => 'cache/PhutilKeyValueCacheInRequest.php',
     'PhutilKeyValueCacheMemcache' => 'cache/PhutilKeyValueCacheMemcache.php',
     'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php',
     'PhutilKeyValueCacheOnDisk' => 'cache/PhutilKeyValueCacheOnDisk.php',
     'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php',
     'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php',
     'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php',
     'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php',
     'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
     'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
     'PhutilLexer' => 'lexer/PhutilLexer.php',
     'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php',
     'PhutilLock' => 'filesystem/PhutilLock.php',
     'PhutilLockException' => 'filesystem/PhutilLockException.php',
     'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
     'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
     'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
     'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php',
     'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php',
     'PhutilNumber' => 'internationalization/PhutilNumber.php',
     'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
     'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
     'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
     'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php',
     'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php',
     'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php',
     'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php',
     'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php',
     'PhutilPHTTestCase' => 'internationalization/__tests__/PhutilPHTTestCase.php',
     'PhutilPerson' => 'internationalization/PhutilPerson.php',
     'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
     'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php',
     'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
     'PhutilProxyException' => 'error/PhutilProxyException.php',
     'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php',
     'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php',
     'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
     'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php',
     'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php',
     'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php',
     'PhutilRemarkupEngineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php',
     'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php',
     'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php',
     'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php',
     'PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule.php',
     'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php',
     'PhutilRemarkupEngineRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php',
     'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php',
     'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php',
     'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php',
     'PhutilRemarkupEngineRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php',
     'PhutilRemarkupEngineRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php',
     'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php',
     'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php',
     'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleBold.php',
     'PhutilRemarkupRuleDel' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleDel.php',
     'PhutilRemarkupRuleDocumentLink' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleDocumentLink.php',
     'PhutilRemarkupRuleEscapeHTML' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeHTML.php',
     'PhutilRemarkupRuleEscapeRemarkup' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeRemarkup.php',
     'PhutilRemarkupRuleHyperlink' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleHyperlink.php',
     'PhutilRemarkupRuleItalic' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleItalic.php',
     'PhutilRemarkupRuleLinebreaks' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleLinebreaks.php',
     'PhutilRemarkupRuleMonospace' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleMonospace.php',
     'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php',
     'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php',
     'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php',
     'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php',
     'PhutilShellLexer' => 'lexer/PhutilShellLexer.php',
     'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php',
     'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php',
     'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php',
     'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php',
     'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php',
     'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php',
     'PhutilSprite' => 'sprites/PhutilSprite.php',
     'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php',
     'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php',
     'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php',
     'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php',
     'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php',
     'PhutilTestCase' => 'infrastructure/testing/PhutilTestCase.php',
     'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
     'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
     'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
     'PhutilURI' => 'parser/PhutilURI.php',
     'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
     'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
     'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
     'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
     'PhutilxsprintfTestCase' => 'xsprintf/__tests__/PhutilxsprintfTestCase.php',
     'QueryFuture' => 'future/query/QueryFuture.php',
     'TempFile' => 'filesystem/TempFile.php',
     'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
     'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
     'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php',
     'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
     'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
     'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
   ),
   'function' =>
   array(
     'Futures' => 'future/functions.php',
     '_qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php',
     '_qsprintf_check_type' => 'xsprintf/qsprintf.php',
     'array_fuse' => 'utils/utils.php',
     'array_interleave' => 'utils/utils.php',
     'array_mergev' => 'utils/utils.php',
     'array_select_keys' => 'utils/utils.php',
     'assert_instances_of' => 'utils/utils.php',
     'assert_stringlike' => 'utils/utils.php',
     'coalesce' => 'utils/utils.php',
     'csprintf' => 'xsprintf/csprintf.php',
     'exec_manual' => 'future/exec/execx.php',
     'execx' => 'future/exec/execx.php',
     'head' => 'utils/utils.php',
     'head_key' => 'utils/utils.php',
     'hgsprintf' => 'xsprintf/hgsprintf.php',
     'hsprintf' => 'markup/render.php',
     'id' => 'utils/utils.php',
     'idx' => 'utils/utils.php',
     'ifilter' => 'utils/utils.php',
     'igroup' => 'utils/utils.php',
     'ipull' => 'utils/utils.php',
     'isort' => 'utils/utils.php',
     'jsprintf' => 'xsprintf/jsprintf.php',
     'last' => 'utils/utils.php',
     'last_key' => 'utils/utils.php',
     'ldap_sprintf' => 'xsprintf/ldapsprintf.php',
     'mfilter' => 'utils/utils.php',
     'mgroup' => 'utils/utils.php',
     'mpull' => 'utils/utils.php',
     'msort' => 'utils/utils.php',
     'newv' => 'utils/utils.php',
     'nonempty' => 'utils/utils.php',
     'phlog' => 'error/phlog.php',
     'pht' => 'internationalization/pht.php',
     'phutil_console_confirm' => 'console/format.php',
     'phutil_console_format' => 'console/format.php',
     'phutil_console_get_terminal_width' => 'console/format.php',
     'phutil_console_prompt' => 'console/format.php',
     'phutil_console_require_tty' => 'console/format.php',
     'phutil_console_wrap' => 'console/format.php',
     'phutil_deprecated' => 'moduleutils/moduleutils.php',
     'phutil_error_listener_example' => 'error/phlog.php',
     'phutil_escape_html' => 'markup/render.php',
     'phutil_escape_html_newlines' => 'markup/render.php',
     'phutil_escape_uri' => 'markup/render.php',
     'phutil_escape_uri_path_component' => 'markup/render.php',
     'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php',
     'phutil_get_library_root' => 'moduleutils/moduleutils.php',
     'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php',
     'phutil_implode_html' => 'markup/render.php',
     'phutil_is_utf8' => 'utils/utf8.php',
     'phutil_passthru' => 'future/exec/execx.php',
     'phutil_render_tag' => 'markup/render.php',
     'phutil_safe_html' => 'markup/render.php',
     'phutil_split_lines' => 'utils/utils.php',
     'phutil_tag' => 'markup/render.php',
     'phutil_unescape_uri_path_component' => 'markup/render.php',
     'phutil_utf8_console_strlen' => 'utils/utf8.php',
     'phutil_utf8_convert' => 'utils/utf8.php',
     'phutil_utf8_hard_wrap' => 'utils/utf8.php',
     'phutil_utf8_hard_wrap_html' => 'utils/utf8.php',
     'phutil_utf8_shorten' => 'utils/utf8.php',
     'phutil_utf8_strlen' => 'utils/utf8.php',
     'phutil_utf8_strtolower' => 'utils/utf8.php',
     'phutil_utf8_strtoupper' => 'utils/utf8.php',
     'phutil_utf8_strtr' => 'utils/utf8.php',
     'phutil_utf8_ucwords' => 'utils/utf8.php',
     'phutil_utf8ize' => 'utils/utf8.php',
     'phutil_utf8v' => 'utils/utf8.php',
     'phutil_utf8v_codepoints' => 'utils/utf8.php',
     'ppull' => 'utils/utils.php',
     'qsprintf' => 'xsprintf/qsprintf.php',
     'queryfx' => 'xsprintf/queryfx.php',
     'queryfx_all' => 'xsprintf/queryfx.php',
     'queryfx_one' => 'xsprintf/queryfx.php',
     'vcsprintf' => 'xsprintf/csprintf.php',
     'vjsprintf' => 'xsprintf/jsprintf.php',
     'vqsprintf' => 'xsprintf/qsprintf.php',
     'vqueryfx' => 'xsprintf/queryfx.php',
     'vqueryfx_all' => 'xsprintf/queryfx.php',
     'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php',
     'xhpast_get_binary_path' => 'parser/xhpast/bin/xhpast_parse.php',
     'xhpast_get_build_instructions' => 'parser/xhpast/bin/xhpast_parse.php',
     'xhpast_get_parser_future' => 'parser/xhpast/bin/xhpast_parse.php',
     'xhpast_is_available' => 'parser/xhpast/bin/xhpast_parse.php',
     'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php',
     'xsprintf' => 'xsprintf/xsprintf.php',
     'xsprintf_callback_example' => 'xsprintf/xsprintf.php',
     'xsprintf_command' => 'xsprintf/csprintf.php',
     'xsprintf_javascript' => 'xsprintf/jsprintf.php',
     'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
     'xsprintf_mercurial' => 'xsprintf/hgsprintf.php',
     'xsprintf_query' => 'xsprintf/qsprintf.php',
   ),
   'xmap' =>
   array(
     'AASTNodeList' =>
     array(
       0 => 'Iterator',
       1 => 'Countable',
     ),
     'AbstractDirectedGraphTestCase' => 'PhutilTestCase',
     'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontMySQLDatabaseConnection' => 'AphrontMySQLDatabaseConnectionBase',
     'AphrontMySQLDatabaseConnectionBase' => 'AphrontDatabaseConnection',
     'AphrontMySQLiDatabaseConnection' => 'AphrontMySQLDatabaseConnectionBase',
     'AphrontQueryAccessDeniedException' => 'AphrontQueryRecoverableException',
     'AphrontQueryConnectionException' => 'AphrontQueryException',
     'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
     'AphrontQueryCountException' => 'AphrontQueryException',
     'AphrontQueryDeadlockException' => 'AphrontQueryRecoverableException',
     'AphrontQueryDuplicateKeyException' => 'AphrontQueryException',
     'AphrontQueryException' => 'Exception',
     'AphrontQueryNotSupportedException' => 'AphrontQueryException',
     'AphrontQueryObjectMissingException' => 'AphrontQueryException',
     'AphrontQueryParameterException' => 'AphrontQueryException',
     'AphrontQueryRecoverableException' => 'AphrontQueryException',
     'AphrontQuerySchemaException' => 'AphrontQueryException',
     'BaseHTTPFuture' => 'Future',
     'CommandException' => 'Exception',
     'ConduitClientException' => 'Exception',
     'ConduitFuture' => 'FutureProxy',
     'ExecFuture' => 'Future',
     'ExecFutureTestCase' => 'PhutilTestCase',
     'FileFinderTestCase' => 'PhutilTestCase',
     'FilesystemException' => 'Exception',
     'FutureIterator' => 'Iterator',
     'FutureIteratorTestCase' => 'PhutilTestCase',
     'FutureProxy' => 'Future',
     'HTTPFuture' => 'BaseHTTPFuture',
     'HTTPFutureResponseStatus' => 'Exception',
     'HTTPFutureResponseStatusCURL' => 'HTTPFutureResponseStatus',
     'HTTPFutureResponseStatusHTTP' => 'HTTPFutureResponseStatus',
     'HTTPFutureResponseStatusParse' => 'HTTPFutureResponseStatus',
     'HTTPFutureResponseStatusTransport' => 'HTTPFutureResponseStatus',
     'HTTPSFuture' => 'BaseHTTPFuture',
     'ImmediateFuture' => 'Future',
     'LinesOfALarge' => 'Iterator',
     'LinesOfALargeExecFuture' => 'LinesOfALarge',
     'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase',
     'LinesOfALargeFile' => 'LinesOfALarge',
     'LinesOfALargeFileTestCase' => 'PhutilTestCase',
+    'PhageAgentTestCase' => 'PhutilTestCase',
+    'PhagePHPAgent' => 'PhageAgent',
+    'PhagePHPAgentBootloader' => 'PhageAgent',
     'PhutilAWSEC2Future' => 'PhutilAWSFuture',
     'PhutilAWSException' => 'Exception',
     'PhutilAWSFuture' => 'FutureProxy',
     'PhutilAggregateException' => 'Exception',
     'PhutilArgumentParserException' => 'Exception',
     'PhutilArgumentParserTestCase' => 'PhutilTestCase',
     'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
     'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase',
     'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
     'PhutilArray' =>
     array(
       0 => 'Phobject',
       1 => 'Countable',
       2 => 'ArrayAccess',
       3 => 'Iterator',
     ),
     'PhutilArrayTestCase' => 'PhutilTestCase',
     'PhutilArrayWithDefaultValue' => 'PhutilArray',
     'PhutilBufferedIterator' => 'Iterator',
     'PhutilBufferedIteratorExample' => 'PhutilBufferedIterator',
     'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
     'PhutilCallbackFilterIterator' => 'FilterIterator',
     'PhutilChannelChannel' => 'PhutilChannel',
     'PhutilChunkedIterator' => 'Iterator',
     'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
     'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
     'PhutilConsoleStdinNotInteractiveException' => 'Exception',
     'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
     'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase',
     'PhutilDeferredLogTestCase' => 'PhutilTestCase',
     'PhutilDocblockParserTestCase' => 'PhutilTestCase',
     'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
     'PhutilEmailAddressTestCase' => 'PhutilTestCase',
     'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
     'PhutilEventType' => 'PhutilEventConstants',
     'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon',
     'PhutilExecChannel' => 'PhutilChannel',
     'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilFileLock' => 'PhutilLock',
     'PhutilFileLockTestCase' => 'PhutilTestCase',
     'PhutilGitURITestCase' => 'PhutilTestCase',
     'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
     'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
     'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilJSONTestCase' => 'PhutilTestCase',
     'PhutilKeyValueCacheAPC' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheDirectory' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheInRequest' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheMemcache' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheOnDisk' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheTestCase' => 'ArcanistPhutilTestCase',
     'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
     'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter',
     'PhutilLockException' => 'Exception',
     'PhutilMarkupTestCase' => 'PhutilTestCase',
     'PhutilMetricsChannel' => 'PhutilChannelChannel',
     'PhutilMissingSymbolException' => 'Exception',
     'PhutilNiceDaemon' => 'PhutilTortureTestDaemon',
     'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase',
     'PhutilPHPFragmentLexer' => 'PhutilLexer',
     'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
     'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase',
     'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilPHTTestCase' => 'PhutilTestCase',
     'PhutilPersonTest' => 'PhutilPerson',
     'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon',
     'PhutilProtocolChannel' => 'PhutilChannelChannel',
     'PhutilProxyException' => 'Exception',
     'PhutilReadableSerializerTestCase' => 'PhutilTestCase',
     'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
     'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupSimpleTableBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineRemarkupTableBlockRule' => 'PhutilRemarkupEngineBlockRule',
     'PhutilRemarkupEngineTestCase' => 'PhutilTestCase',
     'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleDel' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleDocumentLink' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule',
     'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule',
     'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon',
     'PhutilShellLexer' => 'PhutilLexer',
     'PhutilShellLexerTestCase' => 'PhutilTestCase',
     'PhutilSimpleOptionsLexer' => 'PhutilLexer',
     'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
     'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
     'PhutilSocketChannel' => 'PhutilChannel',
     'PhutilSyntaxHighlighterException' => 'Exception',
     'PhutilTestCase' => 'ArcanistPhutilTestCase',
     'PhutilTortureTestDaemon' => 'PhutilDaemon',
     'PhutilTranslatorTestCase' => 'PhutilTestCase',
     'PhutilURITestCase' => 'PhutilTestCase',
     'PhutilUTF8TestCase' => 'PhutilTestCase',
     'PhutilUtilsTestCase' => 'PhutilTestCase',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
     'PhutilxsprintfTestCase' => 'ArcanistTestCase',
     'QueryFuture' => 'Future',
     'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
     'XHPASTNode' => 'AASTNode',
     'XHPASTSyntaxErrorException' => 'Exception',
     'XHPASTToken' => 'AASTToken',
     'XHPASTTree' => 'AASTTree',
     'XHPASTTreeTestCase' => 'PhutilTestCase',
   ),
 ));
diff --git a/src/phage/__tests__/PhageAgentTestCase.php b/src/phage/__tests__/PhageAgentTestCase.php
new file mode 100644
index 0000000..77403a4
--- /dev/null
+++ b/src/phage/__tests__/PhageAgentTestCase.php
@@ -0,0 +1,47 @@
+<?php
+
+final class PhageAgentTestCase extends PhutilTestCase {
+
+  public function testPhagePHPAgent() {
+    return $this->runBootloaderTests(new PhagePHPAgentBootloader());
+  }
+
+  private function runBootloaderTests(PhageAgentBootloader $boot) {
+    $name = get_class($boot);
+
+    $exec = new ExecFuture('%C', $boot->getBootCommand());
+    $exec->write($boot->getBootSequence(), $keep_open = true);
+
+    $exec_channel = new PhutilExecChannel($exec);
+    $agent = new PhutilJSONProtocolChannel($exec_channel);
+
+    $agent->write(
+      array(
+        'type'    => 'EXEC',
+        'key'     => 1,
+        'command' => 'echo phage',
+      ));
+
+    $this->agentExpect(
+      $agent,
+      array(
+        'type'    => 'RSLV',
+        'key'     => 1,
+        'err'     => 0,
+        'stdout'  => "phage\n",
+        'stderr'  => '',
+      ),
+      "'echo phage' for {$name}");
+
+    $agent->write(
+      array(
+        'type'    => 'EXIT',
+      ));
+  }
+
+  private function agentExpect(PhutilChannel $agent, $expect, $what) {
+    $message = $agent->waitForMessage();
+    $this->assertEqual($expect, $message, $what);
+  }
+
+}
diff --git a/src/phage/agent/PhagePHPAgent.php b/src/phage/agent/PhagePHPAgent.php
new file mode 100644
index 0000000..36dabae
--- /dev/null
+++ b/src/phage/agent/PhagePHPAgent.php
@@ -0,0 +1,103 @@
+<?php
+
+final class PhagePHPAgent {
+
+  private $stdin;
+  private $master;
+  private $exec = array();
+
+  public function __construct($stdin) {
+    $this->stdin = $stdin;
+  }
+
+  public function execute() {
+    while (true) {
+      if ($this->exec) {
+        $iterator = new FutureIterator($this->exec);
+        $iterator->setUpdateInterval(0.050);
+        foreach ($iterator as $key => $future) {
+          if ($future === null) {
+            break;
+          }
+          $this->resolveFuture($key, $future);
+          break;
+        }
+      } else {
+        PhutilChannel::waitForAny(array($this->getMaster()));
+      }
+
+      $this->processInput();
+    }
+  }
+
+  private function getMaster() {
+    if (!$this->master) {
+      $raw_channel = new PhutilSocketChannel(
+        $this->stdin,
+        fopen('php://stdout', 'w'));
+
+      $json_channel = new PhutilJSONProtocolChannel($raw_channel);
+      $this->master = $json_channel;
+    }
+
+    return $this->master;
+  }
+
+  private function processInput() {
+    $channel = $this->getMaster();
+
+    $open = $channel->update();
+    if (!$open) {
+      throw new Exception("Channel closed!");
+    }
+
+    while (true) {
+      $command = $channel->read();
+      if ($command === null) {
+        break;
+      }
+      $this->processCommand($command);
+    }
+  }
+
+  private function processCommand(array $spec) {
+    switch ($spec['type']) {
+      case 'EXEC':
+        $key = $spec['key'];
+        $cmd = $spec['command'];
+
+        $future = new ExecFuture('%C', $cmd);
+        $this->exec[$key] = $future;
+        break;
+      case 'EXIT':
+        $this->terminateAgent();
+        break;
+    }
+  }
+
+  private function resolveFuture($key, Future $future) {
+    $result = $future->resolve();
+    $master = $this->getMaster();
+
+    $master->write(
+      array(
+        'type'    => 'RSLV',
+        'key'     => $key,
+        'err'     => $result[0],
+        'stdout'  => $result[1],
+        'stderr'  => $result[2],
+      ));
+  }
+
+  public function __destruct() {
+    $this->terminateAgent();
+  }
+
+  private function terminateAgent() {
+    foreach ($this->exec as $key => $future) {
+      $future->resolveKill();
+    }
+    exit(0);
+  }
+
+}
diff --git a/src/phage/bootloader/PhageAgentBootloader.php b/src/phage/bootloader/PhageAgentBootloader.php
new file mode 100644
index 0000000..c49bd41
--- /dev/null
+++ b/src/phage/bootloader/PhageAgentBootloader.php
@@ -0,0 +1,9 @@
+<?php
+
+abstract class PhageAgentBootloader {
+
+  abstract public function getName();
+  abstract public function getBootCommand();
+  abstract public function getBootSequence();
+
+}
diff --git a/src/phage/bootloader/PhagePHPAgentBootloader.php b/src/phage/bootloader/PhagePHPAgentBootloader.php
new file mode 100644
index 0000000..b44a14c
--- /dev/null
+++ b/src/phage/bootloader/PhagePHPAgentBootloader.php
@@ -0,0 +1,94 @@
+<?php
+
+final class PhagePHPAgentBootloader extends PhageAgentBootloader {
+
+  private $bootSequence;
+  private $bootLength;
+  private $mainLength;
+
+  public function getName() {
+    return 'php';
+  }
+
+  public function getBootCommand() {
+    $this->buildBootSequence();
+    $len = $this->bootLength;
+
+    // We need to run a command which will bootload a full agent by reading
+    // and evaulating source code from stdin. This is the smallest bootstrap
+    // I was able to construct:
+    //
+    //   - Using `fread(STDIN, ...)` is only good up to 8192 bytes.
+    //   - Using `fread(STDIN, ...)` or various other constructs prevents us
+    //     from opening STDIN later.
+    //
+    // Instead, we `fread()` a second-stage bootstrap which has enough code
+    // to do arbitrary-length reads from stdin. The second-stage bootstrap
+    // reads and evaluates the main agent program.
+
+    return csprintf(
+      'php -r %s',
+      "eval(fread(\$I=fopen('php://stdin', 'r'), {$len})); /* phage! */");
+  }
+
+  public function getBootSequence() {
+    $boot = $this->buildBootSequence();
+    return $boot->toString();
+  }
+
+  private function buildBootSequence() {
+    if (!$this->bootSequence) {
+      $files = array(
+        'utils/utils.php',
+        'xsprintf/xsprintf.php',
+        'xsprintf/csprintf.php',
+        'serviceprofiler/PhutilServiceProfiler.php',
+        'future/Future.php',
+        'future/FutureIterator.php',
+        'future/exec/ExecFuture.php',
+        'future/exec/CommandException.php',
+        'channel/PhutilChannel.php',
+        'channel/PhutilSocketChannel.php',
+        'channel/PhutilChannelChannel.php',
+        'channel/PhutilProtocolChannel.php',
+        'channel/PhutilJSONProtocolChannel.php',
+        'phage/agent/PhagePHPAgent.php',
+      );
+
+      $main_sequence = new PhutilBallOfPHP();
+      $root = phutil_get_library_root('phutil');
+      foreach ($files as $file) {
+        $main_sequence->addFile($root.'/'.$file);
+      }
+      $main_sequence->addText('id(new PhagePHPAgent($I))->execute();');
+      $main_length = strlen($main_sequence->toString());
+
+      $boot_sequence = new PhutilBallOfPHP();
+      $boot = '
+        $length = '.$main_length.';
+        $buffer = "";
+        while (strlen($buffer) < $length) {
+          $data = fread($I, $length - strlen($buffer));
+          if (!strlen($data)) {
+            exit(1);
+          }
+          $buffer .= $data;
+        }
+        eval($buffer);';
+      $boot_sequence->addText($boot);
+      $boot_length = strlen($boot_sequence->toString());
+      $boot_sequence->addText($main_sequence->toString());
+
+      if (strlen($boot_length) > 8192) {
+        throw new Exception("Stage 1 bootloader is too large!");
+      }
+
+      $this->bootSequence = $boot_sequence;
+      $this->bootLength = $boot_length;
+      $this->mainLength = $main_length;
+    }
+
+    return $this->bootSequence;
+  }
+
+}
diff --git a/src/phage/util/PhutilBallOfPHP.php b/src/phage/util/PhutilBallOfPHP.php
new file mode 100644
index 0000000..07bdaef
--- /dev/null
+++ b/src/phage/util/PhutilBallOfPHP.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * Concatenates PHP files together into a single stream. Used by Phage to
+ * transmit bootloading code.
+ */
+final class PhutilBallOfPHP {
+
+  private $parts = array();
+
+  public function addFile($path) {
+    $data = Filesystem::readFile($path);
+    if (strncmp($data, "<?php\n", 6)) {
+      throw new Exception(
+        "Expected file '{$path}' to begin \"<?php\\n\".");
+    }
+    $this->parts[] = substr($data, 6);
+    return $this;
+  }
+
+  public function addText($text) {
+    $this->parts[] = $text;
+  }
+
+  public function toString() {
+    return implode('', $this->parts);
+  }
+
+}
diff --git a/src/utils/utils.php b/src/utils/utils.php
index a733a22..e065d71 100644
--- a/src/utils/utils.php
+++ b/src/utils/utils.php
@@ -1,852 +1,869 @@
 <?php
 
 /**
  * Identity function, returns its argument unmodified.
  *
  * This is useful almost exclusively as a workaround to an oddity in the PHP
  * grammar -- this is a syntax error:
  *
  *    COUNTEREXAMPLE
  *    new Thing()->doStuff();
  *
  * ...but this works fine:
  *
  *    id(new Thing())->doStuff();
  *
  * @param   wild Anything.
  * @return  wild Unmodified argument.
  * @group   util
  */
 function id($x) {
   return $x;
 }
 
 
 /**
  * Access an array index, retrieving the value stored there if it exists or
  * a default if it does not. This function allows you to concisely access an
  * index which may or may not exist without raising a warning.
  *
  * @param   array   Array to access.
  * @param   scalar  Index to access in the array.
  * @param   wild    Default value to return if the key is not present in the
  *                  array.
  * @return  wild    If $array[$key] exists, that value is returned. If not,
  *                  $default is returned without raising a warning.
  * @group   util
  */
 function idx(array $array, $key, $default = null) {
   // isset() is a micro-optimization - it is fast but fails for null values.
   if (isset($array[$key])) {
     return $array[$key];
   }
 
   // Comparing $default is also a micro-optimization.
   if ($default === null || array_key_exists($key, $array)) {
     return null;
   }
 
   return $default;
 }
 
 
 /**
  * Call a method on a list of objects. Short for "method pull", this function
  * works just like @{function:ipull}, except that it operates on a list of
  * objects instead of a list of arrays. This function simplifies a common type
  * of mapping operation:
  *
  *    COUNTEREXAMPLE
  *    $names = array();
  *    foreach ($objects as $key => $object) {
  *      $names[$key] = $object->getName();
  *    }
  *
  * You can express this more concisely with mpull():
  *
  *    $names = mpull($objects, 'getName');
  *
  * mpull() takes a third argument, which allows you to do the same but for
  * the array's keys:
  *
  *    COUNTEREXAMPLE
  *    $names = array();
  *    foreach ($objects as $object) {
  *      $names[$object->getID()] = $object->getName();
  *    }
  *
  * This is the mpull version():
  *
  *    $names = mpull($objects, 'getName', 'getID');
  *
  * If you pass ##null## as the second argument, the objects will be preserved:
  *
  *    COUNTEREXAMPLE
  *    $id_map = array();
  *    foreach ($objects as $object) {
  *      $id_map[$object->getID()] = $object;
  *    }
  *
  * With mpull():
  *
  *    $id_map = mpull($objects, null, 'getID');
  *
  * See also @{function:ipull}, which works similarly but accesses array indexes
  * instead of calling methods.
  *
  * @param   list          Some list of objects.
  * @param   string|null   Determines which **values** will appear in the result
  *                        array. Use a string like 'getName' to store the
  *                        value of calling the named method in each value, or
  *                        ##null## to preserve the original objects.
  * @param   string|null   Determines how **keys** will be assigned in the result
  *                        array. Use a string like 'getID' to use the result
  *                        of calling the named method as each object's key, or
  *                        ##null## to preserve the original keys.
  * @return  dict          A dictionary with keys and values derived according
  *                        to whatever you passed as $method and $key_method.
  * @group   util
  */
 function mpull(array $list, $method, $key_method = null) {
   $result = array();
   foreach ($list as $key => $object) {
     if ($key_method !== null) {
       $key = $object->$key_method();
     }
     if ($method !== null) {
       $value = $object->$method();
     } else {
       $value = $object;
     }
     $result[$key] = $value;
   }
   return $result;
 }
 
 
 /**
  * Access a property on a list of objects. Short for "property pull", this
  * function works just like @{function:mpull}, except that it accesses object
  * properties instead of methods. This function simplifies a common type of
  * mapping operation:
  *
  *    COUNTEREXAMPLE
  *    $names = array();
  *    foreach ($objects as $key => $object) {
  *      $names[$key] = $object->name;
  *    }
  *
  * You can express this more concisely with ppull():
  *
  *    $names = ppull($objects, 'name');
  *
  * ppull() takes a third argument, which allows you to do the same but for
  * the array's keys:
  *
  *    COUNTEREXAMPLE
  *    $names = array();
  *    foreach ($objects as $object) {
  *      $names[$object->id] = $object->name;
  *    }
  *
  * This is the ppull version():
  *
  *    $names = ppull($objects, 'name', 'id');
  *
  * If you pass ##null## as the second argument, the objects will be preserved:
  *
  *    COUNTEREXAMPLE
  *    $id_map = array();
  *    foreach ($objects as $object) {
  *      $id_map[$object->id] = $object;
  *    }
  *
  * With ppull():
  *
  *    $id_map = ppull($objects, null, 'id');
  *
  * See also @{function:mpull}, which works similarly but calls object methods
  * instead of accessing object properties.
  *
  * @param   list          Some list of objects.
  * @param   string|null   Determines which **values** will appear in the result
  *                        array. Use a string like 'name' to store the value of
  *                        accessing the named property in each value, or
  *                        ##null## to preserve the original objects.
  * @param   string|null   Determines how **keys** will be assigned in the result
  *                        array. Use a string like 'id' to use the result of
  *                        accessing the named property as each object's key, or
  *                        ##null## to preserve the original keys.
  * @return  dict          A dictionary with keys and values derived according
  *                        to whatever you passed as $property and $key_property.
  * @group   util
  */
 function ppull(array $list, $property, $key_property = null) {
   $result = array();
   foreach ($list as $key => $object) {
     if ($key_property !== null) {
       $key = $object->$key_property;
     }
     if ($property !== null) {
       $value = $object->$property;
     } else {
       $value = $object;
     }
     $result[$key] = $value;
   }
   return $result;
 }
 
 
 /**
  * Choose an index from a list of arrays. Short for "index pull", this function
  * works just like @{function:mpull}, except that it operates on a list of
  * arrays and selects an index from them instead of operating on a list of
  * objects and calling a method on them.
  *
  * This function simplifies a common type of mapping operation:
  *
  *    COUNTEREXAMPLE
  *    $names = array();
  *    foreach ($list as $key => $dict) {
  *      $names[$key] = $dict['name'];
  *    }
  *
  * With ipull():
  *
  *    $names = ipull($list, 'name');
  *
  * See @{function:mpull} for more usage examples.
  *
  * @param   list          Some list of arrays.
  * @param   scalar|null   Determines which **values** will appear in the result
  *                        array. Use a scalar to select that index from each
  *                        array, or null to preserve the arrays unmodified as
  *                        values.
  * @param   scalar|null   Determines which **keys** will appear in the result
  *                        array. Use a scalar to select that index from each
  *                        array, or null to preserve the array keys.
  * @return  dict          A dictionary with keys and values derived according
  *                        to whatever you passed for $index and $key_index.
  * @group   util
  */
 function ipull(array $list, $index, $key_index = null) {
   $result = array();
   foreach ($list as $key => $array) {
     if ($key_index !== null) {
       $key = $array[$key_index];
     }
     if ($index !== null) {
       $value = $array[$index];
     } else {
       $value = $array;
     }
     $result[$key] = $value;
   }
   return $result;
 }
 
 
 /**
  * Group a list of objects by the result of some method, similar to how
  * GROUP BY works in an SQL query. This function simplifies grouping objects
  * by some property:
  *
  *    COUNTEREXAMPLE
  *    $animals_by_species = array();
  *    foreach ($animals as $animal) {
  *      $animals_by_species[$animal->getSpecies()][] = $animal;
  *    }
  *
  * This can be expressed more tersely with mgroup():
  *
  *    $animals_by_species = mgroup($animals, 'getSpecies');
  *
  * In either case, the result is a dictionary which maps species (e.g., like
  * "dog") to lists of animals with that property, so all the dogs are grouped
  * together and all the cats are grouped together, or whatever super
  * businessesey thing is actually happening in your problem domain.
  *
  * See also @{function:igroup}, which works the same way but operates on
  * array indexes.
  *
  * @param   list    List of objects to group by some property.
  * @param   string  Name of a method, like 'getType', to call on each object
  *                  in order to determine which group it should be placed into.
  * @param   ...     Zero or more additional method names, to subgroup the
  *                  groups.
  * @return  dict    Dictionary mapping distinct method returns to lists of
  *                  all objects which returned that value.
  * @group   util
  */
 function mgroup(array $list, $by /* , ... */) {
   $map = mpull($list, $by);
 
   $groups = array();
   foreach ($map as $group) {
     // Can't array_fill_keys() here because 'false' gets encoded wrong.
     $groups[$group] = array();
   }
 
   foreach ($map as $key => $group) {
     $groups[$group][$key] = $list[$key];
   }
 
   $args = func_get_args();
   $args = array_slice($args, 2);
   if ($args) {
     array_unshift($args, null);
     foreach ($groups as $group_key => $grouped) {
       $args[0] = $grouped;
       $groups[$group_key] = call_user_func_array('mgroup', $args);
     }
   }
 
   return $groups;
 }
 
 
 /**
  * Group a list of arrays by the value of some index. This function is the same
  * as @{function:mgroup}, except it operates on the values of array indexes
  * rather than the return values of method calls.
  *
  * @param   list    List of arrays to group by some index value.
  * @param   string  Name of an index to select from each array in order to
  *                  determine which group it should be placed into.
  * @param   ...     Zero or more additional indexes names, to subgroup the
  *                  groups.
  * @return  dict    Dictionary mapping distinct index values to lists of
  *                  all objects which had that value at the index.
  * @group   util
  */
 function igroup(array $list, $by /* , ... */) {
   $map = ipull($list, $by);
 
   $groups = array();
   foreach ($map as $group) {
     $groups[$group] = array();
   }
 
   foreach ($map as $key => $group) {
     $groups[$group][$key] = $list[$key];
   }
 
   $args = func_get_args();
   $args = array_slice($args, 2);
   if ($args) {
     array_unshift($args, null);
     foreach ($groups as $group_key => $grouped) {
       $args[0] = $grouped;
       $groups[$group_key] = call_user_func_array('igroup', $args);
     }
   }
 
   return $groups;
 }
 
 
 /**
  * Sort a list of objects by the return value of some method. In PHP, this is
  * often vastly more efficient than ##usort()## and similar.
  *
  *    // Sort a list of Duck objects by name.
  *    $sorted = msort($ducks, 'getName');
  *
  * It is usually significantly more efficient to define an ordering method
  * on objects and call ##msort()## than to write a comparator. It is often more
  * convenient, as well.
  *
  * NOTE: This method does not take the list by reference; it returns a new list.
  *
  * @param   list    List of objects to sort by some property.
  * @param   string  Name of a method to call on each object; the return values
  *                  will be used to sort the list.
  * @return  list    Objects ordered by the return values of the method calls.
  * @group   util
  */
 function msort(array $list, $method) {
   $surrogate = mpull($list, $method);
 
   asort($surrogate);
 
   $result = array();
   foreach ($surrogate as $key => $value) {
     $result[$key] = $list[$key];
   }
 
   return $result;
 }
 
 
 /**
  * Sort a list of arrays by the value of some index. This method is identical to
  * @{function:msort}, but operates on a list of arrays instead of a list of
  * objects.
  *
  * @param   list    List of arrays to sort by some index value.
  * @param   string  Index to access on each object; the return values
  *                  will be used to sort the list.
  * @return  list    Arrays ordered by the index values.
  * @group   util
  */
 function isort(array $list, $index) {
   $surrogate = ipull($list, $index);
 
   asort($surrogate);
 
   $result = array();
   foreach ($surrogate as $key => $value) {
     $result[$key] = $list[$key];
   }
 
   return $result;
 }
 
 
 /**
  * Filter a list of objects by executing a method across all the objects and
  * filter out the ones wth empty() results. this function works just like
  * @{function:ifilter}, except that it operates on a list of objects instead
  * of a list of arrays.
  *
  * For example, to remove all objects with no children from a list, where
  * 'hasChildren' is a method name, do this:
  *
  *   mfilter($list, 'hasChildren');
  *
  * The optional third parameter allows you to negate the operation and filter
  * out nonempty objects. To remove all objects that DO have children, do this:
  *
  *   mfilter($list, 'hasChildren', true);
  *
  * @param  array        List of objects to filter.
  * @param  string       A method name.
  * @param  bool         Optionally, pass true to drop objects which pass the
  *                      filter instead of keeping them.
  *
  * @return array   List of objects which pass the filter.
  * @group  util
  */
 function mfilter(array $list, $method, $negate = false) {
   if (!is_string($method)) {
     throw new InvalidArgumentException('Argument method is not a string.');
   }
 
   $result = array();
   foreach ($list as $key => $object) {
     $value = $object->$method();
 
     if (!$negate) {
       if (!empty($value)) {
         $result[$key] = $object;
       }
     } else {
       if (empty($value)) {
         $result[$key] = $object;
       }
     }
   }
 
   return $result;
 }
 
 
 /**
  * Filter a list of arrays by removing the ones with an empty() value for some
  * index. This function works just like @{function:mfilter}, except that it
  * operates on a list of arrays instead of a list of objects.
  *
  * For example, to remove all arrays without value for key 'username', do this:
  *
  *   ifilter($list, 'username');
  *
  * The optional third parameter allows you to negate the operation and filter
  * out nonempty arrays. To remove all arrays that DO have value for key
  * 'username', do this:
  *
  *   ifilter($list, 'username', true);
  *
  * @param  array        List of arrays to filter.
  * @param  scalar       The index.
  * @param  bool         Optionally, pass true to drop arrays which pass the
  *                      filter instead of keeping them.
  *
  * @return array   List of arrays which pass the filter.
  * @group  util
  */
 function ifilter(array $list, $index, $negate = false) {
   if (!is_scalar($index)) {
     throw new InvalidArgumentException('Argument index is not a scalar.');
   }
 
   $result = array();
   if (!$negate) {
     foreach ($list as $key => $array) {
       if (!empty($array[$index])) {
         $result[$key] = $array;
       }
     }
   } else {
     foreach ($list as $key => $array) {
       if (empty($array[$index])) {
         $result[$key] = $array;
       }
     }
   }
 
   return $result;
 }
 
 
 /**
  * Selects a list of keys from an array, returning a new array with only the
  * key-value pairs identified by the selected keys, in the specified order.
  *
  * Note that since this function orders keys in the result according to the
  * order they appear in the list of keys, there are effectively two common
  * uses: either reducing a large dictionary to a smaller one, or changing the
  * key order on an existing dictionary.
  *
  * @param  dict    Dictionary of key-value pairs to select from.
  * @param  list    List of keys to select.
  * @return dict    Dictionary of only those key-value pairs where the key was
  *                 present in the list of keys to select. Ordering is
  *                 determined by the list order.
  * @group   util
  */
 function array_select_keys(array $dict, array $keys) {
   $result = array();
   foreach ($keys as $key) {
     if (array_key_exists($key, $dict)) {
       $result[$key] = $dict[$key];
     }
   }
   return $result;
 }
 
 
 /**
  * Checks if all values of array are instances of the passed class.
  * Throws InvalidArgumentException if it isn't true for any value.
  *
  * @param  array
  * @param  string  Name of the class or 'array' to check arrays.
  * @return array   Returns passed array.
  * @group   util
  */
 function assert_instances_of(array $arr, $class) {
   $is_array = !strcasecmp($class, 'array');
 
   foreach ($arr as $key => $object) {
     if ($is_array) {
       if (!is_array($object)) {
         $given = gettype($object);
         throw new InvalidArgumentException(
           "Array item with key '{$key}' must be of type array, ".
           "{$given} given.");
       }
 
     } else if (!($object instanceof $class)) {
       $given = gettype($object);
       if (is_object($object)) {
         $given = 'instance of '.get_class($object);
       }
       throw new InvalidArgumentException(
         "Array item with key '{$key}' must be an instance of {$class}, ".
         "{$given} given.");
     }
   }
 
   return $arr;
 }
 
 /**
  * Assert that passed data can be converted to string.
  *
  * @param  string    Assert that this data is valid.
  * @return void
  *
  * @task   assert
  */
 function assert_stringlike($parameter) {
   switch (gettype($parameter)) {
     case 'string':
     case 'NULL':
     case 'boolean':
     case 'double':
     case 'integer':
       return;
     case 'object':
       if (method_exists($parameter, '__toString')) {
         return;
       }
       break;
     case 'array':
     case 'resource':
     case 'unknown type':
     default:
       break;
   }
 
   throw new InvalidArgumentException(
     "Argument must be scalar or object which implements __toString()!");
 }
 
 /**
  * Returns the first argument which is not strictly null, or ##null## if there
  * are no such arguments. Identical to the MySQL function of the same name.
  *
  * @param  ...         Zero or more arguments of any type.
  * @return mixed       First non-##null## arg, or null if no such arg exists.
  * @group  util
  */
 function coalesce(/* ... */) {
   $args = func_get_args();
   foreach ($args as $arg) {
     if ($arg !== null) {
       return $arg;
     }
   }
   return null;
 }
 
 
 /**
  * Similar to @{function:coalesce}, but less strict: returns the first
  * non-##empty()## argument, instead of the first argument that is strictly
  * non-##null##. If no argument is nonempty, it returns the last argument. This
  * is useful idiomatically for setting defaults:
  *
  *   $display_name = nonempty($user_name, $full_name, "Anonymous");
  *
  * @param  ...         Zero or more arguments of any type.
  * @return mixed       First non-##empty()## arg, or last arg if no such arg
  *                     exists, or null if you passed in zero args.
  * @group  util
  */
 function nonempty(/* ... */) {
   $args = func_get_args();
   $result = null;
   foreach ($args as $arg) {
     $result = $arg;
     if ($arg) {
       break;
     }
   }
   return $result;
 }
 
 
 /**
  * Invokes the "new" operator with a vector of arguments. There is no way to
  * call_user_func_array() on a class constructor, so you can instead use this
  * function:
  *
  *   $obj = newv($class_name, $argv);
  *
  * That is, these two statements are equivalent:
  *
  *   $pancake = new Pancake('Blueberry', 'Maple Syrup', true);
  *   $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true));
  *
  * DO NOT solve this problem in other, more creative ways! Three popular
  * alternatives are:
  *
  *   - Build a fake serialized object and unserialize it.
  *   - Invoke the constructor twice.
  *   - just use eval() lol
  *
  * These are really bad solutions to the problem because they can have side
  * effects (e.g., __wakeup()) and give you an object in an otherwise impossible
  * state. Please endeavor to keep your objects in possible states.
  *
  * If you own the classes you're doing this for, you should consider whether
  * or not restructuring your code (for instance, by creating static
  * construction methods) might make it cleaner before using newv(). Static
  * constructors can be invoked with call_user_func_array(), and may give your
  * class a cleaner and more descriptive API.
  *
  * @param  string  The name of a class.
  * @param  list    Array of arguments to pass to its constructor.
  * @return obj     A new object of the specified class, constructed by passing
  *                 the argument vector to its constructor.
  * @group util
  */
 function newv($class_name, array $argv) {
   $reflector = new ReflectionClass($class_name);
   if ($argv) {
     return $reflector->newInstanceArgs($argv);
   } else {
     return $reflector->newInstance();
   }
 }
 
 
 /**
  * Returns the first element of an array. Exactly like reset(), but doesn't
  * choke if you pass it some non-referenceable value like the return value of
  * a function.
  *
  * @param    array Array to retrieve the first element from.
  * @return   wild  The first value of the array.
  * @group util
  */
 function head(array $arr) {
   return reset($arr);
 }
 
 /**
  * Returns the last element of an array. This is exactly like end() except
  * that it won't warn you if you pass some non-referencable array to
  * it -- e.g., the result of some other array operation.
  *
  * @param    array Array to retrieve the last element from.
  * @return   wild  The last value of the array.
  * @group util
  */
 function last(array $arr) {
   return end($arr);
 }
 
 /**
  * Returns the first key of an array.
  *
  * @param    array       Array to retrieve the first key from.
  * @return   int|string  The first key of the array.
  * @group util
  */
 function head_key(array $arr) {
   reset($arr);
   return key($arr);
 }
 
 /**
  * Returns the last key of an array.
  *
  * @param    array       Array to retrieve the last key from.
  * @return   int|string  The last key of the array.
  * @group util
  */
 function last_key(array $arr) {
   end($arr);
   return key($arr);
 }
 
 /**
  * Merge a vector of arrays performantly. This has the same semantics as
  * array_merge(), so these calls are equivalent:
  *
  *   array_merge($a, $b, $c);
  *   array_mergev(array($a, $b, $c));
  *
  * However, when you have a vector of arrays, it is vastly more performant to
  * merge them with this function than by calling array_merge() in a loop,
  * because using a loop generates an intermediary array on each iteration.
  *
  * @param list Vector of arrays to merge.
  * @return list Arrays, merged with array_merge() semantics.
  * @group util
  */
 function array_mergev(array $arrayv) {
   if (!$arrayv) {
     return array();
   }
 
   return call_user_func_array('array_merge', $arrayv);
 }
 
 
 /**
  * Split a corpus of text into lines. This function splits on "\n", "\r\n", or
  * a mixture of any of them.
  *
  * NOTE: This function does not treat "\r" on its own as a newline because none
  * of SVN, Git or Mercurial do on any OS.
  *
  * @param string Block of text to be split into lines.
  * @param bool If true, retain line endings in result strings.
  * @return list List of lines.
  * @group util
  */
 function phutil_split_lines($corpus, $retain_endings = true) {
   if (!strlen($corpus)) {
     return array('');
   }
 
   // Split on "\r\n" or "\n".
   if ($retain_endings) {
     $lines = preg_split('/(?<=\n)/', $corpus);
   } else {
     $lines = preg_split('/\r?\n/', $corpus);
   }
 
   // If the text ends with "\n" or similar, we'll end up with an empty string
   // at the end; discard it.
   if (end($lines) == '') {
     array_pop($lines);
   }
 
   if ($corpus instanceof PhutilSafeHTML) {
     return array_map('phutil_safe_html', $lines);
   }
 
   return $lines;
 }
 
 
 /**
  * Simplifies a common use of `array_combine()`. Specifically, this:
  *
  *   COUNTEREXAMPLE:
  *   if ($list) {
  *     $result = array_combine($list, $list);
  *   } else {
  *     // Prior to PHP 5.4, array_combine() failed if given empty arrays.
  *     $result = array();
  *   }
  *
  * ...is equivalent to this:
  *
  *   $result = array_fuse($list);
  *
  * @param   list  List of scalars.
  * @return  dict  Dictionary with inputs mapped to themselves.
  * @group util
  */
 function array_fuse(array $list) {
   if ($list) {
     return array_combine($list, $list);
   }
   return array();
 }
 
 
 /**
  * Add an element between every two elements of some array. That is, given a
  * list `A, B, C, D`, and some element to interleave, `x`, this function returns
  * `A, x, B, x, C, x, D`. This works like `implode()`, but does not concatenate
  * the list into a string. In particular:
  *
  *   implode('', array_interleave($x, $list));
  *
  * ...is equivalent to:
  *
  *   implode($x, $list);
  *
  * This function does not preserve keys.
  *
  * @param wild  Element to interleave.
  * @param list  List of elements to be interleaved.
  * @return list Original list with the new element interleaved.
  * @group util
  */
 function array_interleave($interleave, array $array) {
   $result = array();
   foreach ($array as $item) {
     $result[] = $item;
     $result[] = $interleave;
   }
   array_pop($result);
   return $result;
 }
+
+/**
+ * @group library
+ */
+function phutil_is_windows() {
+  // We can also use PHP_OS, but that's kind of sketchy because it returns
+  // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for
+  // DIRECTORY_SEPARATOR is more straightforward.
+  return (DIRECTORY_SEPARATOR != '/');
+}
+
+/**
+ * @group library
+ */
+function phutil_is_hiphop_runtime() {
+  return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1);
+}