diff --git a/bin/aws-s3 b/bin/aws-s3
new file mode 120000
index 0000000..3768bef
--- /dev/null
+++ b/bin/aws-s3
@@ -0,0 +1 @@
+../scripts/utils/aws-s3.php
\ No newline at end of file
diff --git a/scripts/utils/aws-s3.php b/scripts/utils/aws-s3.php
new file mode 100755
index 0000000..10d2b67
--- /dev/null
+++ b/scripts/utils/aws-s3.php
@@ -0,0 +1,22 @@
+#!/usr/bin/env php
+<?php
+
+$root = dirname(dirname(dirname(__FILE__)));
+require_once $root.'/scripts/__init_script__.php';
+
+$args = new PhutilArgumentParser($argv);
+$args->setTagline(pht('AWS CLI Client for S3'));
+$args->setSynopsis(<<<EOSYNOPSIS
+**aws-s3** __command__ [__options__]
+    Upload and download data from Amazon Simple Storage Service (S3).
+
+EOSYNOPSIS
+  );
+$args->parseStandardArguments();
+
+$workflows = id(new PhutilClassMapQuery())
+  ->setAncestorClass('PhutilAWSS3ManagementWorkflow')
+  ->execute();
+
+$workflows[] = new PhutilHelpArgumentWorkflow();
+$args->parseWorkflows($workflows);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index b2ffbb5..d786336 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,914 +1,928 @@
 <?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',
     'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php',
     'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php',
     'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php',
     'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php',
     'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php',
     'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php',
     'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php',
     'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php',
     'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php',
     'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php',
     'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php',
     'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php',
     'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
     'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
     'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php',
     'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php',
     'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php',
     'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php',
     'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php',
     'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php',
     'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
     'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
     'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
     'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
     'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
     'CommandException' => 'future/exec/CommandException.php',
     'ConduitClient' => 'conduit/ConduitClient.php',
     'ConduitClientException' => 'conduit/ConduitClientException.php',
     'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php',
     'ConduitFuture' => 'conduit/ConduitFuture.php',
     'ExecFuture' => 'future/exec/ExecFuture.php',
     'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
     'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php',
     'FileFinder' => 'filesystem/FileFinder.php',
     'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
     'FileList' => 'filesystem/FileList.php',
     'Filesystem' => 'filesystem/Filesystem.php',
     'FilesystemException' => 'filesystem/FilesystemException.php',
     'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php',
     'Future' => 'future/Future.php',
     'FutureIterator' => 'future/FutureIterator.php',
     'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
     'FutureProxy' => 'future/FutureProxy.php',
     'HTTPFuture' => 'future/http/HTTPFuture.php',
     'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php',
     'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php',
     'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php',
     'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php',
     'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
     'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php',
     'HTTPSFuture' => 'future/http/HTTPSFuture.php',
     'ImmediateFuture' => 'future/ImmediateFuture.php',
     'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.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',
     'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.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',
     'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php',
     'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php',
     'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
     'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
     'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
+    'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php',
+    'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php',
     'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
+    'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php',
+    'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php',
+    'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php',
+    'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
+    'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
     'PhutilAggregateException' => 'error/PhutilAggregateException.php',
     'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php',
     'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.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',
     'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php',
     'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
     'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php',
     'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php',
     'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php',
     'PhutilAuthException' => 'auth/exception/PhutilAuthException.php',
     'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php',
     'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php',
     'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php',
     'PhutilBootloader' => 'moduleutils/PhutilBootloader.php',
     'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php',
     'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php',
     'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
     'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
     'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php',
     'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php',
     'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
     'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
     'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
     'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
     'PhutilChannel' => 'channel/PhutilChannel.php',
     'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
     'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php',
     'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
     'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
     'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php',
     'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php',
     'PhutilCommandString' => 'xsprintf/PhutilCommandString.php',
     'PhutilConsole' => 'console/PhutilConsole.php',
     'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php',
     'PhutilConsoleConcatenatedView' => 'console/view/PhutilConsoleConcatenatedView.php',
     'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
     'PhutilConsoleList' => 'console/view/PhutilConsoleList.php',
     'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
     'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php',
     'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
     'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
     'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
     'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
     'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php',
     'PhutilConsoleView' => 'console/view/PhutilConsoleView.php',
     'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
     'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php',
     'PhutilCowsay' => 'utils/PhutilCowsay.php',
     'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php',
     'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php',
     'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php',
     'PhutilDaemon' => 'daemon/PhutilDaemon.php',
     'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php',
     'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php',
     'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.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',
     'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php',
     'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
     'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php',
     'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.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',
     'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php',
     'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php',
     'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
     'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
     'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php',
     'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
     'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php',
     'PhutilErrorTrap' => 'error/PhutilErrorTrap.php',
     'PhutilEvent' => 'events/PhutilEvent.php',
     'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
     'PhutilEventEngine' => 'events/PhutilEventEngine.php',
     'PhutilEventListener' => 'events/PhutilEventListener.php',
     'PhutilEventType' => 'events/constant/PhutilEventType.php',
     'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php',
     'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php',
     'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
     'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php',
     'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php',
     'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php',
     'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php',
     'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
     'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
     'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
     'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
     'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php',
     'PhutilGitURI' => 'parser/PhutilGitURI.php',
     'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
     'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php',
     'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
     'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
     'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
     'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php',
     'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
     'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
     'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
     'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php',
     'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
     'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php',
     'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php',
     'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php',
     'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php',
     'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php',
     'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php',
     'PhutilJSON' => 'parser/PhutilJSON.php',
     'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php',
     'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php',
     'PhutilJSONParser' => 'parser/PhutilJSONParser.php',
     'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php',
     'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php',
     'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
     'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
     'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
     'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php',
     'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php',
     'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php',
     'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php',
     'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php',
     'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php',
     'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php',
     'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php',
     'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php',
     'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
     'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
     'PhutilLexer' => 'lexer/PhutilLexer.php',
     'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php',
     'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php',
     'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php',
     'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php',
     'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php',
     'PhutilLocale' => 'internationalization/PhutilLocale.php',
     'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php',
     'PhutilLock' => 'filesystem/PhutilLock.php',
     'PhutilLockException' => 'filesystem/PhutilLockException.php',
     'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php',
     'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php',
     'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
     'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
     'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
     'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php',
     'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php',
     'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
     'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php',
     'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php',
     'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php',
     'PhutilNumber' => 'internationalization/PhutilNumber.php',
     'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php',
     'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php',
     'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php',
     'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php',
     'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php',
     'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
     'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
     'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
     'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.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',
     'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php',
     'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php',
     'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php',
     'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php',
     'PhutilPerson' => 'internationalization/PhutilPerson.php',
     'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
     'PhutilPersonaAuthAdapter' => 'auth/PhutilPersonaAuthAdapter.php',
     'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php',
     'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php',
     'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php',
     'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php',
     'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php',
     'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
     'PhutilProxyException' => 'error/PhutilProxyException.php',
     'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php',
     'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php',
     'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php',
     'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php',
     'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php',
     'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php',
     'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php',
     'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
     'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php',
     'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php',
     'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php',
     'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php',
     'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php',
     'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php',
     'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php',
     'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php',
     'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php',
     'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php',
     'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php',
     'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php',
     'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php',
     'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php',
     'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
     'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php',
     'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php',
     'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php',
     'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php',
     'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php',
     'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php',
     'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php',
     'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php',
     'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php',
     'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php',
     'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php',
     'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php',
     'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php',
     'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php',
     'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php',
     'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php',
     'PhutilRope' => 'utils/PhutilRope.php',
     'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php',
     'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php',
     'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php',
     'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.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',
     'PhutilSystem' => 'utils/PhutilSystem.php',
     'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php',
     'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php',
     'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php',
     'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
     'PhutilTranslation' => 'internationalization/PhutilTranslation.php',
     'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php',
     'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
     'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
     'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php',
     'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php',
     'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php',
     'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php',
     'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php',
     'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php',
     'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php',
     'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php',
     'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php',
     'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php',
     'PhutilURI' => 'parser/PhutilURI.php',
     'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
     'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
     'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
     'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
     'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
     'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
     'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php',
     'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php',
     'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
     'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php',
     'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php',
     'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php',
     'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php',
     'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
     'QueryFuture' => 'future/query/QueryFuture.php',
     'TempFile' => 'filesystem/TempFile.php',
     'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
     'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
     'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.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',
     'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php',
   ),
   'function' => array(
     '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_censor_credentials' => 'utils/utils.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_count' => 'internationalization/pht.php',
     'phutil_date_format' => 'utils/viewutils.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_fnmatch' => 'utils/utils.php',
     'phutil_format_bytes' => 'utils/viewutils.php',
     'phutil_format_relative_time' => 'utils/viewutils.php',
     'phutil_format_relative_time_detailed' => 'utils/viewutils.php',
     'phutil_format_units_generic' => 'utils/viewutils.php',
     'phutil_fwrite_nonblocking_stream' => 'utils/utils.php',
     'phutil_get_current_library_name' => 'moduleutils/moduleutils.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_get_signal_name' => 'future/exec/execx.php',
     'phutil_hashes_are_identical' => 'utils/utils.php',
     'phutil_implode_html' => 'markup/render.php',
     'phutil_ini_decode' => 'utils/utils.php',
     'phutil_is_hiphop_runtime' => 'utils/utils.php',
     'phutil_is_utf8' => 'utils/utf8.php',
     'phutil_is_utf8_slowly' => 'utils/utf8.php',
     'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php',
     'phutil_is_windows' => 'utils/utils.php',
     'phutil_json_decode' => 'utils/utils.php',
     'phutil_json_encode' => 'utils/utils.php',
     'phutil_load_library' => 'moduleutils/core.php',
     'phutil_loggable_string' => 'utils/utils.php',
     'phutil_parse_bytes' => 'utils/viewutils.php',
     'phutil_passthru' => 'future/exec/execx.php',
     'phutil_register_library' => 'moduleutils/core.php',
     'phutil_register_library_map' => 'moduleutils/core.php',
     'phutil_safe_html' => 'markup/render.php',
     'phutil_split_lines' => 'utils/utils.php',
     'phutil_tag' => 'markup/render.php',
     'phutil_tag_div' => 'markup/render.php',
     'phutil_unescape_uri_path_component' => 'markup/render.php',
     'phutil_units' => 'utils/utils.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_is_combining_character' => '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',
     'phutil_utf8v_combine_characters' => 'utils/utf8.php',
     'phutil_utf8v_combined' => 'utils/utf8.php',
     'phutil_validate_json' => 'utils/utils.php',
     'phutil_var_export' => 'utils/utils.php',
     'ppull' => 'utils/utils.php',
     'pregsprintf' => 'xsprintf/pregsprintf.php',
     'qsprintf' => 'xsprintf/qsprintf.php',
     'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php',
     'qsprintf_check_type' => 'xsprintf/qsprintf.php',
     'queryfx' => 'xsprintf/queryfx.php',
     'queryfx_all' => 'xsprintf/queryfx.php',
     'queryfx_one' => 'xsprintf/queryfx.php',
     'tsprintf' => 'xsprintf/tsprintf.php',
     'urisprintf' => 'xsprintf/urisprintf.php',
     'vcsprintf' => 'xsprintf/csprintf.php',
     'vjsprintf' => 'xsprintf/jsprintf.php',
     'vqsprintf' => 'xsprintf/qsprintf.php',
     'vurisprintf' => 'xsprintf/urisprintf.php',
     'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.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',
     'xsprintf_regex' => 'xsprintf/pregsprintf.php',
     'xsprintf_terminal' => 'xsprintf/tsprintf.php',
     'xsprintf_uri' => 'xsprintf/urisprintf.php',
   ),
   'xmap' => array(
     'AASTNode' => 'Phobject',
     'AASTNodeList' => array(
       'Phobject',
       'Countable',
       'Iterator',
     ),
     'AASTToken' => 'Phobject',
     'AASTTree' => 'Phobject',
     'AbstractDirectedGraph' => 'Phobject',
     'AbstractDirectedGraphTestCase' => 'PhutilTestCase',
     'AphrontAccessDeniedQueryException' => 'AphrontRecoverableQueryException',
     'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontCharacterSetQueryException' => 'AphrontQueryException',
     'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException',
     'AphrontConnectionQueryException' => 'AphrontQueryException',
     'AphrontCountQueryException' => 'AphrontQueryException',
     'AphrontDatabaseConnection' => array(
       'Phobject',
       'PhutilQsprintfInterface',
     ),
     'AphrontDatabaseTransactionState' => 'Phobject',
     'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException',
     'AphrontDuplicateKeyQueryException' => 'AphrontQueryException',
     'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException',
     'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
     'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
     'AphrontNotSupportedQueryException' => 'AphrontQueryException',
     'AphrontObjectMissingQueryException' => 'AphrontQueryException',
     'AphrontParameterQueryException' => 'AphrontQueryException',
     'AphrontQueryException' => 'Exception',
     'AphrontRecoverableQueryException' => 'AphrontQueryException',
     'AphrontSchemaQueryException' => 'AphrontQueryException',
     'AphrontScopedUnguardedWriteCapability' => 'Phobject',
     'AphrontWriteGuard' => 'Phobject',
     'BaseHTTPFuture' => 'Future',
     'CaseInsensitiveArray' => 'PhutilArray',
     'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
     'CommandException' => 'Exception',
     'ConduitClient' => 'Phobject',
     'ConduitClientException' => 'Exception',
     'ConduitClientTestCase' => 'PhutilTestCase',
     'ConduitFuture' => 'FutureProxy',
     'ExecFuture' => 'Future',
     'ExecFutureTestCase' => 'PhutilTestCase',
     'ExecPassthruTestCase' => 'PhutilTestCase',
     'FileFinder' => 'Phobject',
     'FileFinderTestCase' => 'PhutilTestCase',
     'FileList' => 'Phobject',
     'Filesystem' => 'Phobject',
     'FilesystemException' => 'Exception',
     'FilesystemTestCase' => 'PhutilTestCase',
     'Future' => 'Phobject',
     'FutureIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'FutureIteratorTestCase' => 'PhutilTestCase',
     'FutureProxy' => 'Future',
     'HTTPFuture' => 'BaseHTTPFuture',
     'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureResponseStatus' => 'Exception',
     'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPSFuture' => 'BaseHTTPFuture',
     'ImmediateFuture' => 'Future',
     'LibphutilUSEnglishTranslation' => 'PhutilTranslation',
     'LinesOfALarge' => array(
       'Phobject',
       'Iterator',
     ),
     'LinesOfALargeExecFuture' => 'LinesOfALarge',
     'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase',
     'LinesOfALargeFile' => 'LinesOfALarge',
     'LinesOfALargeFileTestCase' => 'PhutilTestCase',
     'MFilterTestHelper' => 'Phobject',
     'PHPASTParserTestCase' => 'PhutilTestCase',
     'PhageAgentBootloader' => 'Phobject',
     'PhageAgentTestCase' => 'PhutilTestCase',
     'PhagePHPAgent' => 'Phobject',
     'PhagePHPAgentBootloader' => 'PhageAgentBootloader',
     'Phobject' => 'Iterator',
     'PhobjectTestCase' => 'PhutilTestCase',
     'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilAWSEC2Future' => 'PhutilAWSFuture',
     'PhutilAWSException' => 'Exception',
     'PhutilAWSFuture' => 'FutureProxy',
+    'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow',
+    'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
     'PhutilAWSS3Future' => 'PhutilAWSFuture',
+    'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
+    'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow',
+    'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
+    'PhutilAWSv4Signature' => 'Phobject',
+    'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
     'PhutilAggregateException' => 'Exception',
     'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
     'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilArgumentParser' => 'Phobject',
     'PhutilArgumentParserException' => 'Exception',
     'PhutilArgumentParserTestCase' => 'PhutilTestCase',
     'PhutilArgumentSpecification' => 'Phobject',
     'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
     'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase',
     'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
     'PhutilArgumentWorkflow' => 'Phobject',
     'PhutilArray' => array(
       'Phobject',
       'Countable',
       'ArrayAccess',
       'Iterator',
     ),
     'PhutilArrayTestCase' => 'PhutilTestCase',
     'PhutilArrayWithDefaultValue' => 'PhutilArray',
     'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilAsanaFuture' => 'FutureProxy',
     'PhutilAuthAdapter' => 'Phobject',
     'PhutilAuthConfigurationException' => 'PhutilAuthException',
     'PhutilAuthCredentialException' => 'PhutilAuthException',
     'PhutilAuthException' => 'Exception',
     'PhutilAuthUserAbortedException' => 'PhutilAuthException',
     'PhutilBallOfPHP' => 'Phobject',
     'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilBootloaderException' => 'Exception',
     'PhutilBritishEnglishLocale' => 'PhutilLocale',
     'PhutilBufferedIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
     'PhutilBugtraqParser' => 'Phobject',
     'PhutilBugtraqParserTestCase' => 'PhutilTestCase',
     'PhutilCIDRBlock' => 'Phobject',
     'PhutilCIDRList' => 'Phobject',
     'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar',
     'PhutilCallbackFilterIterator' => 'FilterIterator',
     'PhutilChannel' => 'Phobject',
     'PhutilChannelChannel' => 'PhutilChannel',
     'PhutilChannelTestCase' => 'PhutilTestCase',
     'PhutilChunkedIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
     'PhutilClassMapQuery' => 'Phobject',
     'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilCommandString' => 'Phobject',
     'PhutilConsole' => 'Phobject',
     'PhutilConsoleBlock' => 'PhutilConsoleView',
     'PhutilConsoleConcatenatedView' => 'PhutilConsoleView',
     'PhutilConsoleFormatter' => 'Phobject',
     'PhutilConsoleList' => 'PhutilConsoleView',
     'PhutilConsoleMessage' => 'Phobject',
     'PhutilConsoleProgressBar' => 'Phobject',
     'PhutilConsoleServer' => 'Phobject',
     'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
     'PhutilConsoleStdinNotInteractiveException' => 'Exception',
     'PhutilConsoleSyntaxHighlighter' => 'Phobject',
     'PhutilConsoleTable' => 'PhutilConsoleView',
     'PhutilConsoleView' => 'Phobject',
     'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
     'PhutilContextFreeGrammar' => 'Phobject',
     'PhutilCowsay' => 'Phobject',
     'PhutilCowsayTestCase' => 'PhutilTestCase',
     'PhutilCsprintfTestCase' => 'PhutilTestCase',
     'PhutilCzechLocale' => 'PhutilLocale',
     'PhutilDaemon' => 'Phobject',
     'PhutilDaemonHandle' => 'Phobject',
     'PhutilDaemonOverseer' => 'Phobject',
     'PhutilDaemonOverseerModule' => 'Phobject',
     'PhutilDefaultSyntaxHighlighter' => 'Phobject',
     'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase',
     'PhutilDeferredLog' => 'Phobject',
     'PhutilDeferredLogTestCase' => 'PhutilTestCase',
     'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
     'PhutilDirectoryFixture' => 'Phobject',
     'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilDivinerSyntaxHighlighter' => 'Phobject',
     'PhutilDocblockParser' => 'Phobject',
     'PhutilDocblockParserTestCase' => 'PhutilTestCase',
     'PhutilEditDistanceMatrix' => 'Phobject',
     'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
     'PhutilEditorConfig' => 'Phobject',
     'PhutilEditorConfigTestCase' => 'PhutilTestCase',
     'PhutilEmailAddress' => 'Phobject',
     'PhutilEmailAddressTestCase' => 'PhutilTestCase',
     'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilErrorHandler' => 'Phobject',
     'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
     'PhutilErrorTrap' => 'Phobject',
     'PhutilEvent' => 'Phobject',
     'PhutilEventConstants' => 'Phobject',
     'PhutilEventEngine' => 'Phobject',
     'PhutilEventListener' => 'Phobject',
     'PhutilEventType' => 'PhutilEventConstants',
     'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator',
     'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon',
     'PhutilExecChannel' => 'PhutilChannel',
     'PhutilExecPassthru' => 'Phobject',
     'PhutilExecutionEnvironment' => 'Phobject',
     'PhutilExtensionsTestCase' => 'PhutilTestCase',
     'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilFileLock' => 'PhutilLock',
     'PhutilFileLockTestCase' => 'PhutilTestCase',
     'PhutilFileTree' => 'Phobject',
     'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilGitURI' => 'Phobject',
     'PhutilGitURITestCase' => 'PhutilTestCase',
     'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
     'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
     'PhutilHgsprintfTestCase' => 'PhutilTestCase',
     'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilINIParserException' => 'Exception',
     'PhutilIPAddress' => 'Phobject',
     'PhutilIPAddressTestCase' => 'PhutilTestCase',
     'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilInteractiveEditor' => 'Phobject',
     'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilInvalidStateException' => 'Exception',
     'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase',
     'PhutilInvisibleSyntaxHighlighter' => 'Phobject',
     'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilJSON' => 'Phobject',
     'PhutilJSONFragmentLexer' => 'PhutilLexer',
     'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
     'PhutilJSONParser' => 'Phobject',
     'PhutilJSONParserException' => 'Exception',
     'PhutilJSONParserTestCase' => 'PhutilTestCase',
     'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilJSONTestCase' => 'PhutilTestCase',
     'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
     'PhutilKeyValueCache' => 'Phobject',
     'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheTestCase' => 'PhutilTestCase',
     'PhutilKoreanLocale' => 'PhutilLocale',
     'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilLanguageGuesser' => 'Phobject',
     'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
     'PhutilLexer' => 'Phobject',
     'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter',
     'PhutilLibraryConflictException' => 'Exception',
     'PhutilLibraryMapBuilder' => 'Phobject',
     'PhutilLibraryTestCase' => 'PhutilTestCase',
     'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilLocale' => 'Phobject',
     'PhutilLocaleTestCase' => 'PhutilTestCase',
     'PhutilLock' => 'Phobject',
     'PhutilLockException' => 'Exception',
     'PhutilLogFileChannel' => 'PhutilChannelChannel',
     'PhutilLunarPhase' => 'Phobject',
     'PhutilLunarPhaseTestCase' => 'PhutilTestCase',
     'PhutilMarkupEngine' => 'Phobject',
     'PhutilMarkupTestCase' => 'PhutilTestCase',
     'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilMethodNotImplementedException' => 'Exception',
     'PhutilMetricsChannel' => 'PhutilChannelChannel',
     'PhutilMissingSymbolException' => 'Exception',
     'PhutilModuleUtilsTestCase' => 'PhutilTestCase',
     'PhutilNiceDaemon' => 'PhutilTortureTestDaemon',
     'PhutilNumber' => 'Phobject',
     'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter',
     'PhutilOAuth1Future' => 'FutureProxy',
     'PhutilOAuth1FutureTestCase' => 'PhutilTestCase',
     'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilOpaqueEnvelope' => 'Phobject',
     'PhutilOpaqueEnvelopeKey' => 'Phobject',
     'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase',
     'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
     'PhutilPHPFragmentLexer' => 'PhutilLexer',
     'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
     'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase',
     'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilParserGenerator' => 'Phobject',
     'PhutilParserGeneratorException' => 'Exception',
     'PhutilParserGeneratorTestCase' => 'PhutilTestCase',
     'PhutilPayPalAPIFuture' => 'FutureProxy',
     'PhutilPersonTest' => array(
       'Phobject',
       'PhutilPerson',
     ),
     'PhutilPersonaAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilPhtTestCase' => 'PhutilTestCase',
     'PhutilPirateEnglishLocale' => 'PhutilLocale',
     'PhutilPregsprintfTestCase' => 'PhutilTestCase',
     'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon',
     'PhutilProtocolChannel' => 'PhutilChannelChannel',
     'PhutilProxyException' => 'Exception',
     'PhutilPygmentsSyntaxHighlighter' => 'Phobject',
     'PhutilPythonFragmentLexer' => 'PhutilLexer',
     'PhutilQueryStringParser' => 'Phobject',
     'PhutilQueryStringParserTestCase' => 'PhutilTestCase',
     'PhutilRainbowSyntaxHighlighter' => 'Phobject',
     'PhutilRawEnglishLocale' => 'PhutilLocale',
     'PhutilReadableSerializer' => 'Phobject',
     'PhutilReadableSerializerTestCase' => 'PhutilTestCase',
     'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilRemarkupBlockInterpreter' => 'Phobject',
     'PhutilRemarkupBlockRule' => 'Phobject',
     'PhutilRemarkupBlockStorage' => 'Phobject',
     'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupDelRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
     'PhutilRemarkupEngineTestCase' => 'PhutilTestCase',
     'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupRule' => 'Phobject',
     'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter',
     'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule',
     'PhutilRope' => 'Phobject',
     'PhutilRopeTestCase' => 'PhutilTestCase',
     'PhutilSafeHTML' => 'Phobject',
     'PhutilSafeHTMLTestCase' => 'PhutilTestCase',
     'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon',
     'PhutilServiceProfiler' => 'Phobject',
     'PhutilShellLexer' => 'PhutilLexer',
     'PhutilShellLexerTestCase' => 'PhutilTestCase',
     'PhutilSimpleOptions' => 'Phobject',
     'PhutilSimpleOptionsLexer' => 'PhutilLexer',
     'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
     'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
     'PhutilSocketChannel' => 'PhutilChannel',
     'PhutilSprite' => 'Phobject',
     'PhutilSpriteSheet' => 'Phobject',
     'PhutilSyntaxHighlighter' => 'Phobject',
     'PhutilSyntaxHighlighterEngine' => 'Phobject',
     'PhutilSyntaxHighlighterException' => 'Exception',
     'PhutilSystem' => 'Phobject',
     'PhutilSystemTestCase' => 'PhutilTestCase',
     'PhutilTerminalString' => 'Phobject',
     'PhutilTestPhobject' => 'Phobject',
     'PhutilTortureTestDaemon' => 'PhutilDaemon',
     'PhutilTranslation' => 'Phobject',
     'PhutilTranslationTestCase' => 'PhutilTestCase',
     'PhutilTranslator' => 'Phobject',
     'PhutilTranslatorTestCase' => 'PhutilTestCase',
     'PhutilTsprintfTestCase' => 'PhutilTestCase',
     'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilTwitchFuture' => 'FutureProxy',
     'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilTypeCheckException' => 'Exception',
     'PhutilTypeExtraParametersException' => 'Exception',
     'PhutilTypeLexer' => 'PhutilLexer',
     'PhutilTypeMissingParametersException' => 'Exception',
     'PhutilTypeSpec' => 'Phobject',
     'PhutilTypeSpecTestCase' => 'PhutilTestCase',
     'PhutilURI' => 'Phobject',
     'PhutilURITestCase' => 'PhutilTestCase',
     'PhutilUSEnglishLocale' => 'PhutilLocale',
     'PhutilUTF8StringTruncator' => 'Phobject',
     'PhutilUTF8TestCase' => 'PhutilTestCase',
     'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUrisprintfTestCase' => 'PhutilTestCase',
     'PhutilUtilsTestCase' => 'PhutilTestCase',
     'PhutilVeryWowEnglishLocale' => 'PhutilLocale',
     'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilWordPressFuture' => 'FutureProxy',
     'PhutilXHPASTBinary' => 'Phobject',
     'PhutilXHPASTSyntaxHighlighter' => 'Phobject',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
     'QueryFuture' => 'Future',
     'TempFile' => 'Phobject',
     'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
     'XHPASTNode' => 'AASTNode',
     'XHPASTNodeTestCase' => 'PhutilTestCase',
     'XHPASTSyntaxErrorException' => 'Exception',
     'XHPASTToken' => 'AASTToken',
     'XHPASTTree' => 'AASTTree',
     'XHPASTTreeTestCase' => 'PhutilTestCase',
     'XsprintfUnknownConversionException' => 'InvalidArgumentException',
   ),
 ));
diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php
index 6d0b979..54432d5 100644
--- a/src/future/aws/PhutilAWSFuture.php
+++ b/src/future/aws/PhutilAWSFuture.php
@@ -1,147 +1,168 @@
 <?php
 
 abstract class PhutilAWSFuture extends FutureProxy {
 
   private $future;
-  private $awsAccessKey;
-  private $awsPrivateKey;
-  private $awsRegion;
-  private $builtRequest;
-  private $params;
+  private $accessKey;
+  private $secretKey;
+  private $region;
+  private $httpMethod = 'GET';
+  private $path = '/';
+  private $params = array();
+  private $endpoint;
+  private $data = '';
+  private $headers = array();
 
   abstract public function getServiceName();
 
   public function __construct() {
     parent::__construct(null);
   }
 
-  public function setAWSKeys($access, $private) {
-    $this->awsAccessKey = $access;
-    $this->awsPrivateKey = $private;
+  public function setAccessKey($access_key) {
+    $this->accessKey = $access_key;
     return $this;
   }
 
-  public function getAWSAccessKey() {
-    return $this->awsAccessKey;
+  public function getAccessKey() {
+    return $this->accessKey;
   }
 
-  public function getAWSPrivateKey() {
-    return $this->awsPrivateKey;
+  public function setSecretKey(PhutilOpaqueEnvelope $secret_key) {
+    $this->secretKey = $secret_key;
+    return $this;
+  }
+
+  public function getSecretKey() {
+    return $this->secretKey;
   }
 
-  public function getAWSRegion() {
-    return $this->awsRegion;
+  public function getRegion() {
+    return $this->region;
+  }
+
+  public function setRegion($region) {
+    $this->region = $region;
+    return $this;
   }
 
-  public function setAWSRegion($region) {
-    $this->awsRegion = $region;
+  public function setEndpoint($endpoint) {
+    $this->endpoint = $endpoint;
     return $this;
   }
 
-  public function getHost() {
-    $host = $this->getServiceName().'.'.$this->awsRegion.'.amazonaws.com';
-    return $host;
+  public function getEndpoint() {
+    return $this->endpoint;
   }
 
-  public function setRawAWSQuery($action, array $params = array()) {
-    $this->params = $params;
-    $this->params['Action'] = $action;
+  public function setHTTPMethod($method) {
+    $this->httpMethod = $method;
     return $this;
   }
 
-  protected function getProxiedFuture() {
-    if (!$this->future) {
-      $params = $this->params;
+  public function getHTTPMethod() {
+    return $this->httpMethod;
+  }
 
-      if (!$this->params) {
-        throw new Exception(
-          pht(
-            'You must %s!',
-            'setRawAWSQuery()'));
-      }
+  public function setPath($path) {
+    $this->path = $path;
+    return $this;
+  }
 
-      if (!$this->getAWSAccessKey()) {
-        throw new Exception(
-          pht(
-            'You must %s!',
-            'setAWSKeys()'));
-      }
+  public function getPath() {
+    return $this->path;
+  }
+
+  public function setData($data) {
+    $this->data = $data;
+    return $this;
+  }
+
+  public function getData() {
+    return $this->data;
+  }
+
+  protected function getParameters() {
+    $params = $this->params;
+    return $params;
+  }
 
-      $params['AWSAccessKeyId'] = $this->getAWSAccessKey();
-      $params['Version']        = '2013-10-15';
-      $params['Timestamp']      = date('c');
+  public function addHeader($key, $value) {
+    $this->headers[] = array($key, $value);
+    return $this;
+  }
 
-      $params = $this->sign($params);
+  protected function getProxiedFuture() {
+    if (!$this->future) {
+      $params = $this->getParameters();
+      $method = $this->getHTTPMethod();
+      $host = $this->getEndpoint();
+      $path = $this->getPath();
+      $data = $this->getData();
+
+      $uri = id(new PhutilURI("https://{$host}/"))
+        ->setPath($path)
+        ->setQueryParams($params);
+
+      $future = id(new HTTPSFuture($uri, $data))
+        ->setMethod($method);
+
+      foreach ($this->headers as $header) {
+        list($key, $value) = $header;
+        $future->addHeader($key, $value);
+      }
 
-      $uri = new PhutilURI('http://'.$this->getHost().'/');
-      $uri->setQueryParams($params);
+      $this->signRequest($future);
 
-      $this->future = new HTTPFuture($uri);
+      $this->future = $future;
     }
 
     return $this->future;
   }
 
+  protected function signRequest(HTTPSFuture $future) {
+    $access_key = $this->getAccessKey();
+    $secret_key = $this->getSecretKey();
+
+    $region = $this->getRegion();
+
+    id(new PhutilAWSv4Signature())
+      ->setRegion($region)
+      ->setService($this->getServiceName())
+      ->setAccessKey($access_key)
+      ->setSecretKey($secret_key)
+      ->signRequest($future);
+  }
+
   protected function didReceiveResult($result) {
     list($status, $body, $headers) = $result;
 
     try {
       $xml = @(new SimpleXMLElement($body));
     } catch (Exception $ex) {
       $xml = null;
     }
 
     if ($status->isError() || !$xml) {
       if (!($status instanceof HTTPFutureHTTPResponseStatus)) {
         throw $status;
       }
 
       $params = array(
         'body' => $body,
       );
       if ($xml) {
         $params['RequestID'] = $xml->RequestID[0];
-        foreach ($xml->Errors[0] as $error) {
+        $errors = array($xml->Error);
+        foreach ($errors as $error) {
           $params['Errors'][] = array($error->Code, $error->Message);
         }
       }
 
       throw new PhutilAWSException($status->getStatusCode(), $params);
     }
 
     return $xml;
   }
 
-  /**
-   * http://bit.ly/wU0JFh
-   */
-  private function sign(array $params) {
-
-    $params['SignatureMethod'] = 'HmacSHA256';
-    $params['SignatureVersion'] = '2';
-
-    ksort($params);
-
-    $pstr = array();
-    foreach ($params as $key => $value) {
-      $pstr[] = rawurlencode($key).'='.rawurlencode($value);
-    }
-    $pstr = implode('&', $pstr);
-
-    $sign = "GET"."\n".
-            strtolower($this->getHost())."\n".
-            "/"."\n".
-            $pstr;
-
-    $hash = hash_hmac(
-      'sha256',
-      $sign,
-      $this->getAWSPrivateKey(),
-      $raw_ouput = true);
-
-    $params['Signature'] = base64_encode($hash);
-
-    return $params;
-  }
-
 }
diff --git a/src/future/aws/PhutilAWSS3Future.php b/src/future/aws/PhutilAWSS3Future.php
index b3e7c3e..6941829 100644
--- a/src/future/aws/PhutilAWSS3Future.php
+++ b/src/future/aws/PhutilAWSS3Future.php
@@ -1,9 +1,66 @@
 <?php
 
 final class PhutilAWSS3Future extends PhutilAWSFuture {
 
+  private $bucket;
+
   public function getServiceName() {
     return 's3';
   }
 
+  public function setBucket($bucket) {
+    $this->bucket = $bucket;
+    return $this;
+  }
+
+  public function getBucket() {
+    return $this->bucket;
+  }
+
+  public function setParametersForGetObject($key) {
+    $bucket = $this->getBucket();
+
+    $this->setHTTPMethod('GET');
+    $this->setPath($bucket.'/'.$key);
+
+    return $this;
+  }
+
+  public function setParametersForPutObject($key, $value) {
+    $bucket = $this->getBucket();
+
+    $this->setHTTPMethod('PUT');
+    $this->setPath($bucket.'/'.$key);
+
+    $this->addHeader('X-Amz-ACL', 'private');
+    $this->addHeader('Content-Type', 'application/octet-stream');
+
+    $this->setData($value);
+
+    return $this;
+  }
+
+  public function setParametersForDeleteObject($key) {
+    $bucket = $this->getBucket();
+
+    $this->setHTTPMethod('DELETE');
+    $this->setPath($bucket.'/'.$key);
+
+    return $this;
+  }
+
+  protected function didReceiveResult($result) {
+    list($status, $body, $headers) = $result;
+
+    if (!$status->isError()) {
+      return $body;
+    }
+
+    if ($status->getStatusCode() === 404) {
+      return null;
+    }
+
+    return parent::didReceiveResult($result);
+  }
+
 }
diff --git a/src/future/aws/PhutilAWSv4Signature.php b/src/future/aws/PhutilAWSv4Signature.php
new file mode 100644
index 0000000..50248b0
--- /dev/null
+++ b/src/future/aws/PhutilAWSv4Signature.php
@@ -0,0 +1,256 @@
+<?php
+
+final class PhutilAWSv4Signature extends Phobject {
+
+  private $accessKey;
+  private $secretKey;
+  private $signingKey;
+
+  private $date;
+
+  private $region;
+  private $service;
+
+  public function setAccessKey($access_key) {
+    $this->accessKey = $access_key;
+    return $this;
+  }
+
+  public function setSecretKey(PhutilOpaqueEnvelope $secret_key) {
+    $this->secretKey = $secret_key;
+    return $this;
+  }
+
+  public function setDate($date) {
+    $this->date = $date;
+    return $this;
+  }
+
+  public function getDate() {
+    if ($this->date === null) {
+      $this->date = gmdate('Ymd\THis\Z', time());
+    }
+    return $this->date;
+  }
+
+  public function setRegion($region) {
+    $this->region = $region;
+    return $this;
+  }
+
+  public function getRegion() {
+    return $this->region;
+  }
+
+  public function setService($service) {
+    $this->service = $service;
+    return $this;
+  }
+
+  public function getService() {
+    return $this->service;
+  }
+
+  public function setSigningKey($signing_key) {
+    $this->signingKey = $signing_key;
+    return $this;
+  }
+
+  public function getSigningKey() {
+    if ($this->signingKey === null) {
+      $this->signingKey = $this->computeSigningKey();
+    }
+
+    return $this->signingKey;
+  }
+
+  private function getAlgorithm() {
+    return 'AWS4-HMAC-SHA256';
+  }
+
+  private function getHost(HTTPSFuture $future) {
+    $uri = new PhutilURI($future->getURI());
+    return $uri->getDomain();
+  }
+
+  private function getPath(HTTPSFuture $future) {
+    $uri = new PhutilURI($future->getURI());
+    return $uri->getPath();
+  }
+
+  public function signRequest(HTTPSFuture $future) {
+    $body_signature = $this->getBodySignature($future);
+
+    $future->addHeader('X-Amz-Content-sha256', $body_signature);
+    $future->addHeader('X-Amz-Date', $this->getDate());
+
+    $request_signature = $this->getCanonicalRequestSignature(
+      $future,
+      $body_signature);
+
+    $string_to_sign = $this->getStringToSign($request_signature);
+
+    $signing_key = $this->getSigningKey();
+
+    $signature = hash_hmac('sha256', $string_to_sign, $signing_key);
+
+    $algorithm = $this->getAlgorithm();
+    $credential = $this->getCredential();
+    $signed_headers = $this->getSignedHeaderList($future);
+
+    $authorization =
+      $algorithm.' '.
+      'Credential='.$credential.','.
+      'SignedHeaders='.$signed_headers.','.
+      'Signature='.$signature;
+
+    $future->addHeader('Authorization', $authorization);
+
+    return $future;
+  }
+
+  private function getBodySignature(HTTPSFuture $future) {
+    $http_body = $future->getData();
+
+    if (is_array($http_body)) {
+      $http_body = '';
+    }
+
+    return hash('sha256', $http_body);
+  }
+
+  private function getCanonicalRequestSignature(
+    HTTPSFuture $future,
+    $body_signature) {
+
+    $http_method = $future->getMethod();
+
+    $path = $this->getPath($future);
+    $path = rawurlencode($path);
+    $path = str_replace('%2F', '/', $path);
+
+    $canonical_parameters = $this->getCanonicalParameterList($future);
+    $canonical_headers = $this->getCanonicalHeaderList($future);
+    $signed_headers = $this->getSignedHeaderList($future);
+
+    $canonical_request =
+      $http_method."\n".
+      $path."\n".
+      $canonical_parameters."\n".
+      $canonical_headers."\n".
+      "\n".
+      $signed_headers."\n".
+      $body_signature;
+
+    return hash('sha256', $canonical_request);
+  }
+
+  private function getStringToSign($request_signature) {
+    $algorithm = $this->getAlgorithm();
+    $date = $this->getDate();
+    $scope_parts = $this->getScopeParts();
+    $scope = implode('/', $scope_parts);
+
+    $string_to_sign =
+      $algorithm."\n".
+      $date."\n".
+      $scope."\n".
+      $request_signature;
+
+    return $string_to_sign;
+  }
+
+  private function getScopeParts() {
+    return array(
+      substr($this->getDate(), 0, 8),
+      $this->getRegion(),
+      $this->getService(),
+      'aws4_request',
+    );
+  }
+
+  private function computeSigningKey() {
+    $secret_key = $this->secretKey;
+    if (!$secret_key) {
+      throw new Exception(
+        pht(
+          'You must either provide a signing key with setSigningKey(), or '.
+          'provide a secret key with setSecretKey().'));
+    }
+
+    // NOTE: This part of the algorithm uses the raw binary hashes, and the
+    // result is not human-readable.
+    $raw_hash = true;
+
+    $signing_key = 'AWS4'.$secret_key->openEnvelope();
+
+    $scope_parts = $this->getScopeParts();
+    foreach ($scope_parts as $scope_part) {
+      $signing_key = hash_hmac('sha256', $scope_part, $signing_key, $raw_hash);
+    }
+
+    return $signing_key;
+  }
+
+  private function getCanonicalHeaderList(HTTPSFuture $future) {
+    $headers = $this->getCanonicalHeaderMap($future);
+
+    $canonical_headers = array();
+    foreach ($headers as $header => $header_value) {
+      $canonical_headers[] = $header.':'.trim($header_value);
+    }
+
+    return implode("\n", $canonical_headers);
+  }
+
+  private function getCanonicalHeaderMap(HTTPSFuture $future) {
+    $headers = $future->getHeaders();
+    $headers[] = array(
+      'Host',
+      $this->getHost($future),
+    );
+
+    $header_map = array();
+    foreach ($headers as $header) {
+      list($key, $value) = $header;
+      $key = phutil_utf8_strtolower($key);
+      $header_map[$key] = $value;
+    }
+
+    ksort($header_map);
+
+    return $header_map;
+  }
+
+  private function getSignedHeaderList(HTTPSFuture $future) {
+    $headers = $this->getCanonicalHeaderMap($future);
+    return implode(';', array_keys($headers));
+  }
+
+  private function getCanonicalParameterList(HTTPSFuture $future) {
+    $uri = new PhutilURI($future->getURI());
+    $params = $uri->getQueryParams();
+
+    ksort($params);
+    $canonical_parameters = array();
+    foreach ($params as $key => $value) {
+      $canonical_parameters[] = rawurlencode($key).'='.rawurlencode($value);
+    }
+    $canonical_parameters = implode('&', $canonical_parameters);
+
+    return $canonical_parameters;
+  }
+
+  private function getCredential() {
+    $access_key = $this->accessKey;
+    if (!strlen($access_key)) {
+      throw new PhutilInvalidStateException('setAccessKey');
+    }
+
+    $parts = $this->getScopeParts();
+    array_unshift($parts, $access_key);
+
+    return implode('/', $parts);
+  }
+
+}
diff --git a/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php
new file mode 100644
index 0000000..f09dbf1
--- /dev/null
+++ b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php
@@ -0,0 +1,159 @@
+<?php
+
+final class PhutilAWSv4SignatureTestCase extends PhutilTestCase {
+
+
+  public function testAWSv4SignaturesS3GetObject() {
+    $access_key = 'AKIAIOSFODNN7EXAMPLE';
+    $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+    $date = '20130524T000000Z';
+    $region = 'us-east-1';
+    $service = 's3';
+    $uri = 'https://examplebucket.s3.amazonaws.com/test.txt';
+    $method = 'GET';
+
+    $future = id(new HTTPSFuture($uri))
+      ->setMethod($method)
+      ->addHeader('Range', 'bytes=0-9');
+
+    $signature = id(new PhutilAWSv4Signature())
+      ->setAccessKey($access_key)
+      ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+      ->setDate($date)
+      ->setRegion($region)
+      ->setService($service);
+
+    $signature->signRequest($future);
+
+    $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,
+Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41
+EOSIGNATURE;
+
+    $this->assertSignature($expect, $future);
+  }
+
+
+  public function testAWSv4SignaturesS3PutObject() {
+    $access_key = 'AKIAIOSFODNN7EXAMPLE';
+    $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+    $date = '20130524T000000Z';
+    $region = 'us-east-1';
+    $service = 's3';
+    $uri = 'https://examplebucket.s3.amazonaws.com/test$file.text';
+    $method = 'PUT';
+    $body = 'Welcome to Amazon S3.';
+
+    $future = id(new HTTPSFuture($uri, $body))
+      ->setMethod($method)
+      ->addHeader('X-Amz-Storage-Class', 'REDUCED_REDUNDANCY')
+      ->addHeader('Date', 'Fri, 24 May 2013 00:00:00 GMT');
+
+    $signature = id(new PhutilAWSv4Signature())
+      ->setAccessKey($access_key)
+      ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+      ->setDate($date)
+      ->setRegion($region)
+      ->setService($service);
+
+    $signature->signRequest($future);
+
+    $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,
+Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd
+EOSIGNATURE;
+
+    $this->assertSignature($expect, $future);
+  }
+
+
+  public function testAWSv4SignaturesS3GetBucketLifecycle() {
+    $access_key = 'AKIAIOSFODNN7EXAMPLE';
+    $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+    $date = '20130524T000000Z';
+    $region = 'us-east-1';
+    $service = 's3';
+    $uri = 'https://examplebucket.s3.amazonaws.com/?lifecycle';
+    $method = 'GET';
+
+    $future = id(new HTTPSFuture($uri))
+      ->setMethod($method);
+
+    $signature = id(new PhutilAWSv4Signature())
+      ->setAccessKey($access_key)
+      ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+      ->setDate($date)
+      ->setRegion($region)
+      ->setService($service);
+
+    $signature->signRequest($future);
+
+    $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=host;x-amz-content-sha256;x-amz-date,
+Signature=fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543
+EOSIGNATURE;
+
+    $this->assertSignature($expect, $future);
+  }
+
+
+  public function testAWSv4SignaturesS3GetBucket() {
+    $access_key = 'AKIAIOSFODNN7EXAMPLE';
+    $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+    $date = '20130524T000000Z';
+    $region = 'us-east-1';
+    $service = 's3';
+    $uri = 'https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J';
+    $method = 'GET';
+
+    $future = id(new HTTPSFuture($uri))
+      ->setMethod($method);
+
+    $signature = id(new PhutilAWSv4Signature())
+      ->setAccessKey($access_key)
+      ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+      ->setDate($date)
+      ->setRegion($region)
+      ->setService($service);
+
+    $signature->signRequest($future);
+
+    $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=host;x-amz-content-sha256;x-amz-date,
+Signature=34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7
+EOSIGNATURE;
+
+    $this->assertSignature($expect, $future);
+  }
+
+
+  private function assertSignature($expect, HTTPSFuture $signed) {
+    $authorization = null;
+    foreach ($signed->getHeaders() as $header) {
+      list($key, $value) = $header;
+      if (phutil_utf8_strtolower($key) === 'authorization') {
+        $authorization = $value;
+        break;
+      }
+    }
+
+    $expect = str_replace("\n\n", ' ', $expect);
+    $expect = str_replace("\n", '', $expect);
+
+    $this->assertEqual($expect, $authorization);
+  }
+
+
+}
diff --git a/src/future/aws/management/PhutilAWSManagementWorkflow.php b/src/future/aws/management/PhutilAWSManagementWorkflow.php
new file mode 100644
index 0000000..b7f5661
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSManagementWorkflow.php
@@ -0,0 +1,83 @@
+<?php
+
+abstract class PhutilAWSManagementWorkflow
+  extends PhutilArgumentWorkflow {
+
+  public function isExecutable() {
+    return true;
+  }
+
+  protected function newAWSFuture($template) {
+    $argv = $this->getArgv();
+
+    $access_key = $argv->getArg('access-key');
+    $secret_key = $argv->getArg('secret-key');
+
+    $has_root = (strlen($access_key) || strlen($secret_key));
+    if ($has_root) {
+      if (!strlen($access_key) || !strlen($secret_key)) {
+        throw new PhutilArgumentUsageException(
+          pht(
+            'When specifying AWS credentials with --access-key and '.
+            '--secret-key, you must provide both keys.'));
+      }
+
+      $template->setAccessKey($access_key);
+      $template->setSecretKey(new PhutilOpaqueEnvelope($secret_key));
+    }
+
+    $has_any = ($has_root);
+    if (!$has_any) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'You must specify AWS credentials. Use --access-key and '.
+          '--secret-key to provide root credentials.'));
+    }
+
+    $region = $argv->getArg('region');
+    if (!strlen($region)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'You must specify an AWS region with --region.'));
+    }
+
+    $template->setRegion($region);
+
+    $endpoint = $argv->getArg('endpoint');
+    if (!strlen($endpoint)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'You must specify an AWS endpoint with --endpoint.'));
+    }
+
+    $template->setEndpoint($endpoint);
+
+    return $template;
+  }
+
+  protected function getAWSArguments() {
+    return array(
+      array(
+        'name' => 'access-key',
+        'param' => 'key',
+        'help' => pht('AWS access key.'),
+      ),
+      array(
+        'name' => 'secret-key',
+        'param' => 'file',
+        'help' => pht('AWS secret key.'),
+      ),
+      array(
+        'name' => 'region',
+        'param' => 'region',
+        'help' => pht('AWS region.'),
+      ),
+      array(
+        'name' => 'endpoint',
+        'param' => 'endpoint',
+        'help' => pht('Name of the AWS region to access.'),
+      ),
+    );
+  }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php
new file mode 100644
index 0000000..f2f1e8d
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php
@@ -0,0 +1,45 @@
+<?php
+
+final class PhutilAWSS3DeleteManagementWorkflow
+  extends PhutilAWSS3ManagementWorkflow {
+
+  protected function didConstruct() {
+    $this
+      ->setName('delete')
+      ->setExamples(
+        '**delete** --key __key__')
+      ->setSynopsis(pht('Delete an object from S3.'))
+      ->setArguments(
+        array_merge(
+          $this->getAWSArguments(),
+          $this->getAWSS3BucketArguments(),
+          array(
+            array(
+              'name'    => 'key',
+              'param'   => 'key',
+              'help'    => pht('Specify a key to delete.'),
+            ),
+          )));
+  }
+
+  public function execute(PhutilArgumentParser $args) {
+    $key = $args->getArg('key');
+    if (!strlen($key)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 object key to access with --key.'));
+    }
+
+    $future = $this->newAWSFuture(new PhutilAWSS3Future())
+      ->setParametersForDeleteObject($key);
+
+    $future->resolve();
+
+    echo tsprintf(
+      "%s\n",
+      pht('Deleted "%s".', $key));
+
+    return 0;
+  }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php
new file mode 100644
index 0000000..fab9af1
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php
@@ -0,0 +1,57 @@
+<?php
+
+final class PhutilAWSS3GetManagementWorkflow
+  extends PhutilAWSS3ManagementWorkflow {
+
+  protected function didConstruct() {
+    $this
+      ->setName('get')
+      ->setExamples(
+        '**get** --key __key__')
+      ->setSynopsis(pht('Download content from S3.'))
+      ->setArguments(
+        array_merge(
+          $this->getAWSArguments(),
+          $this->getAWSS3BucketArguments(),
+          array(
+            array(
+              'name'    => 'key',
+              'param'   => 'key',
+              'help'    => pht('Specify a key to retrieve.'),
+            ),
+          )));
+  }
+
+  public function execute(PhutilArgumentParser $args) {
+    $bucket = $args->getArg('bucket');
+    if (!strlen($bucket)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 bucket to access with --bucket.'));
+    }
+
+    $endpoint = $args->getArg('endpoint');
+    if (!strlen($endpoint)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 endpoint with --endpoint.'));
+    }
+
+    $key = $args->getArg('key');
+    if (!strlen($key)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 object key to access with --key.'));
+    }
+
+    $future = $this->newAWSFuture(new PhutilAWSS3Future())
+      ->setBucket($bucket)
+      ->setEndpoint($endpoint)
+      ->setParametersForGetObject($key);
+
+    echo $future->resolve();
+
+    return 0;
+  }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php
new file mode 100644
index 0000000..35d3253
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php
@@ -0,0 +1,27 @@
+<?php
+
+abstract class PhutilAWSS3ManagementWorkflow
+  extends PhutilAWSManagementWorkflow {
+
+  protected function getAWSS3BucketArguments() {
+    return array(
+      array(
+        'name' => 'bucket',
+        'param' => 'bucket',
+        'help' => pht('Name of the S3 bucket to access.'),
+      ),
+    );
+  }
+
+  protected function newAWSFuture($future) {
+    $future = parent::newAWSFuture($future);
+
+    $argv = $this->getArgv();
+    $bucket = $argv->getArg('bucket');
+
+    $future->setBucket($bucket);
+
+    return $future;
+  }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3PutManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3PutManagementWorkflow.php
new file mode 100644
index 0000000..199bf12
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3PutManagementWorkflow.php
@@ -0,0 +1,52 @@
+<?php
+
+final class PhutilAWSS3PutManagementWorkflow
+  extends PhutilAWSS3ManagementWorkflow {
+
+  protected function didConstruct() {
+    $this
+      ->setName('put')
+      ->setExamples(
+        '**put** --key __key__')
+      ->setSynopsis(pht('Upload content to S3.'))
+      ->setArguments(
+        array_merge(
+          $this->getAWSArguments(),
+          $this->getAWSS3BucketArguments(),
+          array(
+            array(
+              'name'    => 'key',
+              'param'   => 'key',
+              'help'    => pht('Specify a key to upload.'),
+            ),
+          )));
+  }
+
+  public function execute(PhutilArgumentParser $args) {
+    $key = $args->getArg('key');
+    if (!strlen($key)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 object key to access with --key.'));
+    }
+
+    $future = $this->newAWSFuture(new PhutilAWSS3Future());
+
+    echo tsprintf(
+      "%s\n",
+      pht('Reading data from stdin...'));
+
+    $data = file_get_contents('php://stdin');
+
+    $future->setParametersForPutObject($key, $data);
+
+    $result = $future->resolve();
+
+    echo tsprintf(
+      "%s\n",
+      pht('Uploaded "%s".', $key));
+
+    return 0;
+  }
+
+}
diff --git a/src/markup/engine/__tests__/remarkup/highlight.txt b/src/markup/engine/__tests__/remarkup/highlight.txt
index e0c78ee..5fb8895 100644
--- a/src/markup/engine/__tests__/remarkup/highlight.txt
+++ b/src/markup/engine/__tests__/remarkup/highlight.txt
@@ -1,7 +1,9 @@
 how about we !!highlight!! some !!TEXT!!!
 wow this must be **!!very important!!**
+omg!!!!!
 ~~~~~~~~~~
 <p>how about we <span class="remarkup-highlight">highlight</span> some <span class="remarkup-highlight">TEXT!</span>
-wow this must be <strong><span class="remarkup-highlight">very important</span></strong></p>
+wow this must be <strong><span class="remarkup-highlight">very important</span></strong>
+omg!!!!!</p>
 ~~~~~~~~~~
-how about we !!highlight!! some !!TEXT!!! wow this must be **!!very important!!**
+how about we !!highlight!! some !!TEXT!!! wow this must be **!!very important!!** omg!!!!!
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
index 9abbb9b..900e355 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
+++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
@@ -1,30 +1,37 @@
 <?php
 
 final class PhutilRemarkupHighlightRule extends PhutilRemarkupRule {
 
   public function getPriority() {
     return 1000.0;
   }
 
   public function apply($text) {
     if ($this->getEngine()->isTextMode()) {
       return $text;
     }
 
     return $this->replaceHTML(
       '@!!(.+?)(!{2,})@',
       array($this, 'applyCallback'),
       $text);
   }
 
   protected function applyCallback(array $matches) {
     // Remove the two exclamation points that represent syntax.
     $excitement = substr($matches[2], 2);
 
+    // If the internal content consists of ONLY exclamation points, leave it
+    // untouched so "!!!!!" is five exclamation points instead of one
+    // highlighted exclamation point.
+    if (preg_match('/^!+\z/', $matches[1])) {
+      return $matches[0];
+    }
+
     // $excitement now has two fewer !'s than we started with.
     return hsprintf('<span class="remarkup-highlight">%s%s</span>',
       $matches[1], $excitement);
 
   }
 
 }
diff --git a/src/parser/argument/PhutilArgumentParser.php b/src/parser/argument/PhutilArgumentParser.php
index 7a4ae93..706be52 100644
--- a/src/parser/argument/PhutilArgumentParser.php
+++ b/src/parser/argument/PhutilArgumentParser.php
@@ -1,825 +1,827 @@
 <?php
 
 /**
  * Parser for command-line arguments for scripts. Like similar parsers, this
  * class allows you to specify, validate, and render help for command-line
  * arguments. For example:
  *
  *   name=create_dog.php
  *   $args = new PhutilArgumentParser($argv);
  *   $args->setTagline('make an new dog')
  *   $args->setSynopsis(<<<EOHELP
  *   **dog** [--big] [--name __name__]
  *   Create a new dog. How does it work? Who knows.
  *   EOHELP
  *   );
  *   $args->parse(
  *     array(
  *       array(
  *         'name'     => 'name',
  *         'param'    => 'dogname',
  *         'default'  => 'Rover',
  *         'help'     => 'Set the dog\'s name. By default, the dog will be '.
  *                       'named "Rover".',
  *       ),
  *       array(
  *         'name'     => 'big',
  *         'short'    => 'b',
  *         'help'     => 'If set, create a large dog.',
  *       ),
  *     ));
  *
  *   $dog_name = $args->getArg('name');
  *   $dog_size = $args->getArg('big') ? 'big' : 'small';
  *
  *   // ... etc ...
  *
  * (For detailed documentation on supported keys in argument specifications,
  * see @{class:PhutilArgumentSpecification}.)
  *
  * This will handle argument parsing, and generate appropriate usage help if
  * the user provides an unsupported flag. @{class:PhutilArgumentParser} also
  * supports some builtin "standard" arguments:
  *
  *   $args->parseStandardArguments();
  *
  * See @{method:parseStandardArguments} for details. Notably, this includes
  * a "--help" flag, and an "--xprofile" flag for profiling command-line scripts.
  *
  * Normally, when the parser encounters an unknown flag, it will exit with
  * an error. However, you can use @{method:parsePartial} to consume only a
  * set of flags:
  *
  *   $args->parsePartial($spec_list);
  *
  * This allows you to parse some flags before making decisions about other
  * parsing, or share some flags across scripts. The builtin standard arguments
  * are implemented in this way.
  *
  * There is also builtin support for "workflows", which allow you to build a
  * script that operates in several modes (e.g., by accepting commands like
  * `install`, `upgrade`, etc), like `arc` does. For detailed documentation on
  * workflows, see @{class:PhutilArgumentWorkflow}.
  *
  * @task parse    Parsing Arguments
  * @task read     Reading Arguments
  * @task help     Command Help
  * @task internal Internals
  */
 final class PhutilArgumentParser extends Phobject {
 
   private $bin;
   private $argv;
   private $specs = array();
   private $results = array();
   private $parsed;
 
   private $tagline;
   private $synopsis;
   private $workflows;
   private $showHelp;
 
   const PARSE_ERROR_CODE = 77;
 
 
 /* -(  Parsing Arguments  )-------------------------------------------------- */
 
 
   /**
    * Build a new parser. Generally, you start a script with:
    *
    *   $args = new PhutilArgumentParser($argv);
    *
    * @param list  Argument vector to parse, generally the $argv global.
    * @task parse
    */
   public function __construct(array $argv) {
     $this->bin = $argv[0];
     $this->argv = array_slice($argv, 1);
   }
 
 
   /**
    * Parse and consume a list of arguments, removing them from the argument
    * vector but leaving unparsed arguments for later consumption. You can
    * retrieve unconsumed arguments directly with
    * @{method:getUnconsumedArgumentVector}. Doing a partial parse can make it
    * easier to share common flags across scripts or workflows.
    *
    * @param   list  List of argument specs, see
    *                @{class:PhutilArgumentSpecification}.
    * @return  this
    * @task parse
    */
   public function parsePartial(array $specs) {
     $specs = PhutilArgumentSpecification::newSpecsFromList($specs);
     $this->mergeSpecs($specs);
 
     $specs_by_name  = mpull($specs, null, 'getName');
     $specs_by_short = mpull($specs, null, 'getShortAlias');
     unset($specs_by_short[null]);
 
     $argv = $this->argv;
     $len = count($argv);
     for ($ii = 0; $ii < $len; $ii++) {
       $arg = $argv[$ii];
       $map = null;
       if (!is_string($arg)) {
         // Non-string argument; pass it through as-is.
       } else if ($arg == '--') {
         // This indicates "end of flags".
         break;
       } else if ($arg == '-') {
         // This is a normal argument (e.g., stdin).
         continue;
       } else if (!strncmp('--', $arg, 2)) {
         $pre = '--';
         $arg = substr($arg, 2);
         $map = $specs_by_name;
       } else if (!strncmp('-', $arg, 1) && strlen($arg) > 1) {
         $pre = '-';
         $arg = substr($arg, 1);
         $map = $specs_by_short;
       }
 
       if ($map) {
         $val = null;
         $parts = explode('=', $arg, 2);
         if (count($parts) == 2) {
           list($arg, $val) = $parts;
         }
 
         if (isset($map[$arg])) {
           $spec = $map[$arg];
           unset($argv[$ii]);
 
           $param_name = $spec->getParamName();
           if ($val !== null) {
             if ($param_name === null) {
               throw new PhutilArgumentUsageException(
                 pht(
                   "Argument '%s' does not take a parameter.",
                   "{$pre}{$arg}"));
             }
           } else {
             if ($param_name !== null) {
               if ($ii + 1 < $len) {
                 $val = $argv[$ii + 1];
                 unset($argv[$ii + 1]);
                 $ii++;
               } else {
                 throw new PhutilArgumentUsageException(
                   pht(
                     "Argument '%s' requires a parameter.",
                     "{$pre}{$arg}"));
               }
             } else {
               $val = true;
             }
           }
 
           if (!$spec->getRepeatable()) {
             if (array_key_exists($spec->getName(), $this->results)) {
               throw new PhutilArgumentUsageException(
                 pht(
                   "Argument '%s' was provided twice.",
                   "{$pre}{$arg}"));
             }
           }
 
           $conflicts = $spec->getConflicts();
           foreach ($conflicts as $conflict => $reason) {
             if (array_key_exists($conflict, $this->results)) {
 
               if (!is_string($reason) || !strlen($reason)) {
                 $reason = '.';
               } else {
                 $reason = ': '.$reason.'.';
               }
 
               throw new PhutilArgumentUsageException(
                 pht(
                   "Argument '%s' conflicts with argument '%s'%s",
                   "{$pre}{$arg}",
                   "--{$conflict}",
                   $reason));
             }
           }
 
           if ($spec->getRepeatable()) {
             if ($spec->getParamName() === null) {
               if (empty($this->results[$spec->getName()])) {
                 $this->results[$spec->getName()] = 0;
               }
               $this->results[$spec->getName()]++;
             } else {
               $this->results[$spec->getName()][] = $val;
             }
           } else {
             $this->results[$spec->getName()] = $val;
           }
         }
       }
     }
 
     foreach ($specs as $spec) {
       if ($spec->getWildcard()) {
         $this->results[$spec->getName()] = $this->filterWildcardArgv($argv);
         $argv = array();
         break;
       }
     }
 
     $this->argv = array_values($argv);
     return $this;
   }
 
 
   /**
    * Parse and consume a list of arguments, throwing an exception if there is
    * anything left unconsumed. This is like @{method:parsePartial}, but raises
    * a {class:PhutilArgumentUsageException} if there are leftovers.
    *
    * Normally, you would call @{method:parse} instead, which emits a
    * user-friendly error. You can also use @{method:printUsageException} to
    * render the exception in a user-friendly way.
    *
    * @param   list  List of argument specs, see
    *                @{class:PhutilArgumentSpecification}.
    * @return  this
    * @task parse
    */
   public function parseFull(array $specs) {
     $this->parsePartial($specs);
 
     if (count($this->argv)) {
       $arg = head($this->argv);
       throw new PhutilArgumentUsageException(
         pht("Unrecognized argument '%s'.", $arg));
     }
 
     if ($this->showHelp) {
       $this->printHelpAndExit();
     }
 
     return $this;
   }
 
 
   /**
    * Parse and consume a list of arguments, raising a user-friendly error if
    * anything remains. See also @{method:parseFull} and @{method:parsePartial}.
    *
    * @param   list  List of argument specs, see
    *                @{class:PhutilArgumentSpecification}.
    * @return  this
    * @task parse
    */
   public function parse(array $specs) {
     try {
       return $this->parseFull($specs);
     } catch (PhutilArgumentUsageException $ex) {
       $this->printUsageException($ex);
       exit(self::PARSE_ERROR_CODE);
     }
   }
 
 
   /**
    * Parse and execute workflows, raising a user-friendly error if anything
    * remains. See also @{method:parseWorkflowsFull}.
    *
    * See @{class:PhutilArgumentWorkflow} for details on using workflows.
    *
    * @param   list  List of argument specs, see
    *                @{class:PhutilArgumentSpecification}.
    * @return  this
    * @task parse
    */
   public function parseWorkflows(array $workflows) {
     try {
       return $this->parseWorkflowsFull($workflows);
     } catch (PhutilArgumentUsageException $ex) {
       $this->printUsageException($ex);
       exit(self::PARSE_ERROR_CODE);
     }
   }
 
 
   /**
    * Select a workflow. For commands that may operate in several modes, like
    * `arc`, the modes can be split into "workflows". Each workflow specifies
    * the arguments it accepts. This method takes a list of workflows, selects
    * the chosen workflow, parses its arguments, and either executes it (if it
    * is executable) or returns it for handling.
    *
    * See @{class:PhutilArgumentWorkflow} for details on using workflows.
    *
    * @param list List of @{class:PhutilArgumentWorkflow}s.
    * @return PhutilArgumentWorkflow|no  Returns the chosen workflow if it is
    *                                    not executable, or executes it and
    *                                    exits with a return code if it is.
    * @task parse
    */
   public function parseWorkflowsFull(array $workflows) {
     assert_instances_of($workflows, 'PhutilArgumentWorkflow');
 
     // Clear out existing workflows. We need to do this to permit the
     // construction of sub-workflows.
     $this->workflows = array();
 
     foreach ($workflows as $workflow) {
       $name = $workflow->getName();
 
       if ($name === null) {
         throw new PhutilArgumentSpecificationException(
           pht('Workflow has no name!'));
       }
 
       if (isset($this->workflows[$name])) {
         throw new PhutilArgumentSpecificationException(
           pht("Two workflows with name '%s!", $name));
       }
 
       $this->workflows[$name] = $workflow;
     }
 
     $argv = $this->argv;
     if (empty($argv)) {
       // TODO: this is kind of hacky / magical.
       if (isset($this->workflows['help'])) {
         $argv = array('help');
       } else {
         throw new PhutilArgumentUsageException(pht('No workflow selected.'));
       }
     }
 
     $flow = array_shift($argv);
     $flow = strtolower($flow);
 
     if (empty($this->workflows[$flow])) {
       $workflow_names = array();
       foreach ($this->workflows as $wf) {
         $workflow_names[] = $wf->getName();
       }
       sort($workflow_names);
       $command_list = implode(', ', $workflow_names);
       $ex_msg = pht(
         "Invalid command '%s'. Valid commands are: %s.",
         $flow,
         $command_list);
       if (in_array('help', $workflow_names)) {
         $bin = basename($this->bin);
         $ex_msg .= "\n".pht(
           'For more details on available commands, run `%s`.', "{$bin} help");
       }
       throw new PhutilArgumentUsageException($ex_msg);
     }
 
     $workflow = $this->workflows[$flow];
 
     if ($this->showHelp) {
       // Make "cmd flow --help" behave like "cmd help flow", not "cmd help".
       $help_flow = idx($this->workflows, 'help');
       if ($help_flow) {
         if ($help_flow !== $workflow) {
           $workflow = $help_flow;
           $argv = array($flow);
 
           // Prevent parse() from dumping us back out to standard help.
           $this->showHelp = false;
         }
       } else {
         $this->printHelpAndExit();
       }
     }
 
     $this->argv = array_values($argv);
 
     if ($workflow->shouldParsePartial()) {
       $this->parsePartial($workflow->getArguments());
     } else {
       $this->parse($workflow->getArguments());
     }
 
+
     if ($workflow->isExecutable()) {
+      $workflow->setArgv($this);
       $err = $workflow->execute($this);
       exit($err);
     } else {
       return $workflow;
     }
   }
 
 
   /**
    * Parse "standard" arguments and apply their effects:
    *
    *    --trace             Enable service call tracing.
    *    --no-ansi           Disable ANSI color/style sequences.
    *    --xprofile <file>   Write out an XHProf profile.
    *    --help              Show help.
    *
    * @return this
    *
    * @phutil-external-symbol function xhprof_enable
    */
   public function parseStandardArguments() {
     try {
       $this->parsePartial(
         array(
           array(
             'name'  => 'trace',
             'help'  => pht('Trace command execution and show service calls.'),
             'standard' => true,
           ),
           array(
             'name'  => 'no-ansi',
             'help'  => pht(
               'Disable ANSI terminal codes, printing plain text with '.
               'no color or style.'),
             'conflicts' => array(
               'ansi' => null,
             ),
             'standard' => true,
           ),
           array(
             'name'  => 'ansi',
             'help'  => pht(
               "Use formatting even in environments which probably ".
               "don't support it."),
             'standard' => true,
           ),
           array(
             'name'  => 'xprofile',
             'param' => 'profile',
             'help'  => pht(
               'Profile script execution and write results to a file.'),
             'standard' => true,
           ),
           array(
             'name'  => 'help',
             'short' => 'h',
             'help'  => pht('Show this help.'),
             'standard' => true,
           ),
           array(
             'name'  => 'show-standard-options',
             'help'  => pht(
               'Show every option, including standard options like this one.'),
             'standard' => true,
           ),
           array(
             'name'  => 'recon',
             'help'  => pht('Start in remote console mode.'),
             'standard' => true,
           ),
         ));
     } catch (PhutilArgumentUsageException $ex) {
       $this->printUsageException($ex);
       exit(self::PARSE_ERROR_CODE);
     }
 
     if ($this->getArg('trace')) {
       PhutilServiceProfiler::installEchoListener();
     }
 
     if ($this->getArg('no-ansi')) {
       PhutilConsoleFormatter::disableANSI(true);
     }
 
     if ($this->getArg('ansi')) {
       PhutilConsoleFormatter::disableANSI(false);
     }
 
     if ($this->getArg('help')) {
       $this->showHelp = true;
     }
 
     $xprofile = $this->getArg('xprofile');
     if ($xprofile) {
       if (!function_exists('xhprof_enable')) {
         throw new Exception(
           pht("To use '%s', you must install XHProf.", '--xprofile'));
       }
 
       xhprof_enable(0);
       register_shutdown_function(array($this, 'shutdownProfiler'));
     }
 
     $recon = $this->getArg('recon');
     if ($recon) {
       $remote_console = PhutilConsole::newRemoteConsole();
       $remote_console->beginRedirectOut();
       PhutilConsole::setConsole($remote_console);
     } else if ($this->getArg('trace')) {
       $server = new PhutilConsoleServer();
       $server->setEnableLog(true);
       $console = PhutilConsole::newConsoleForServer($server);
       PhutilConsole::setConsole($console);
     }
 
     return $this;
   }
 
 
 /* -(  Reading Arguments  )-------------------------------------------------- */
 
 
   public function getArg($name) {
     if (empty($this->specs[$name])) {
       throw new PhutilArgumentSpecificationException(
         pht("No specification exists for argument '%s'!", $name));
     }
 
     if (idx($this->results, $name) !== null) {
       return $this->results[$name];
     }
 
     return $this->specs[$name]->getDefault();
   }
 
   public function getUnconsumedArgumentVector() {
     return $this->argv;
   }
 
 
 /* -(  Command Help  )------------------------------------------------------- */
 
 
   public function setSynopsis($synopsis) {
     $this->synopsis = $synopsis;
     return $this;
   }
 
   public function setTagline($tagline) {
     $this->tagline = $tagline;
     return $this;
   }
 
   public function printHelpAndExit() {
     echo $this->renderHelp();
     exit(self::PARSE_ERROR_CODE);
   }
 
   public function renderHelp() {
     $out = array();
     $more = array();
 
     if ($this->bin) {
       $out[] = $this->format('**%s**', pht('NAME'));
       $name = $this->indent(6, '**%s**', basename($this->bin));
       if ($this->tagline) {
         $name .= $this->format(' - '.$this->tagline);
       }
       $out[] = $name;
       $out[] = null;
     }
 
     if ($this->synopsis) {
       $out[] = $this->format('**%s**', pht('SYNOPSIS'));
       $out[] = $this->indent(6, $this->synopsis);
       $out[] = null;
     }
 
     if ($this->workflows) {
       $has_help = false;
       $out[] = $this->format('**%s**', pht('WORKFLOWS'));
       $out[] = null;
       $flows = $this->workflows;
       ksort($flows);
       foreach ($flows as $workflow) {
         if ($workflow->getName() == 'help') {
           $has_help = true;
         }
         $out[] = $this->renderWorkflowHelp(
           $workflow->getName(),
           $show_details = false);
       }
       if ($has_help) {
         $more[] = pht(
           'Use **%s** __command__ for a detailed command reference.', 'help');
       }
     }
 
     $specs = $this->renderArgumentSpecs($this->specs);
     if ($specs) {
       $out[] = $this->format('**%s**', pht('OPTION REFERENCE'));
       $out[] = null;
       $out[] = $specs;
     }
 
     // If we have standard options but no --show-standard-options, print out
     // a quick hint about it.
     if (!empty($this->specs['show-standard-options']) &&
         !$this->getArg('show-standard-options')) {
       $more[] = pht(
         'Use __%s__ to show additional options.', '--show-standard-options');
     }
 
     $out[] = null;
 
     if ($more) {
       foreach ($more as $hint) {
         $out[] = $this->indent(0, $hint);
       }
       $out[] = null;
     }
 
     return implode("\n", $out);
   }
 
   public function renderWorkflowHelp(
     $workflow_name,
     $show_details = false) {
 
     $out = array();
 
     $indent = ($show_details ? 0 : 6);
 
     $workflow = idx($this->workflows, strtolower($workflow_name));
     if (!$workflow) {
       $out[] = $this->indent(
         $indent,
         pht('There is no **%s** workflow.', $workflow_name));
     } else {
       $out[] = $this->indent($indent, $workflow->getExamples());
       $out[] = $this->indent($indent, $workflow->getSynopsis());
       if ($show_details) {
         $full_help = $workflow->getHelp();
         if ($full_help) {
           $out[] = null;
           $out[] = $this->indent($indent, $full_help);
         }
         $specs = $this->renderArgumentSpecs($workflow->getArguments());
         if ($specs) {
           $out[] = null;
           $out[] = $specs;
         }
       }
     }
 
     $out[] = null;
 
     return implode("\n", $out);
   }
 
   public function printUsageException(PhutilArgumentUsageException $ex) {
     fwrite(
       STDERR,
       $this->format("**%s** %s\n", pht('Usage Exception:'), $ex->getMessage()));
   }
 
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   private function filterWildcardArgv(array $argv) {
     foreach ($argv as $key => $value) {
       if ($value == '--') {
         unset($argv[$key]);
         break;
       } else if (
         is_string($value) &&
         !strncmp($value, '-', 1) &&
         strlen($value) > 1) {
 
         throw new PhutilArgumentUsageException(
           pht(
             "Argument '%s' is unrecognized. Use '%s' to indicate ".
             "the end of flags.",
             $value,
             '--'));
       }
     }
     return array_values($argv);
   }
 
   private function mergeSpecs(array $specs) {
 
     $short_map = mpull($this->specs, null, 'getShortAlias');
     unset($short_map[null]);
 
     $wildcard = null;
     foreach ($this->specs as $spec) {
       if ($spec->getWildcard()) {
         $wildcard = $spec;
         break;
       }
     }
 
     foreach ($specs as $spec) {
       $spec->validate();
       $name = $spec->getName();
 
       if (isset($this->specs[$name])) {
         throw new PhutilArgumentSpecificationException(
           pht("Two argument specifications have the same name ('%s').", $name));
       }
 
       $short = $spec->getShortAlias();
       if ($short) {
         if (isset($short_map[$short])) {
           throw new PhutilArgumentSpecificationException(
             pht(
               "Two argument specifications have the same short alias ('%s').",
               $short));
         }
         $short_map[$short] = $spec;
       }
 
       if ($spec->getWildcard()) {
         if ($wildcard) {
           throw new PhutilArgumentSpecificationException(
             pht(
               'Two argument specifications are marked as wildcard arguments. '.
               'You can have a maximum of one wildcard argument.'));
         } else {
           $wildcard = $spec;
         }
       }
 
       $this->specs[$name] = $spec;
     }
 
     foreach ($this->specs as $name => $spec) {
       foreach ($spec->getConflicts() as $conflict => $reason) {
         if (empty($this->specs[$conflict])) {
           throw new PhutilArgumentSpecificationException(
             pht(
               "Argument '%s' conflicts with unspecified argument '%s'.",
               $name,
               $conflict));
         }
         if ($conflict == $name) {
           throw new PhutilArgumentSpecificationException(
             pht("Argument '%s' conflicts with itself!", $name));
         }
       }
     }
 
   }
 
   private function renderArgumentSpecs(array $specs) {
     foreach ($specs as $key => $spec) {
       if ($spec->getWildcard()) {
         unset($specs[$key]);
       }
     }
 
     $out = array();
 
     $no_standard_options =
       !empty($this->specs['show-standard-options']) &&
       !$this->getArg('show-standard-options');
 
     $specs = msort($specs, 'getName');
     foreach ($specs as $spec) {
       if ($spec->getStandard() && $no_standard_options) {
         // If this is a standard argument and the user didn't pass
         // --show-standard-options, skip it.
         continue;
       }
       $name = $this->indent(6, '__--%s__', $spec->getName());
       $short = null;
       if ($spec->getShortAlias()) {
         $short = $this->format(', __-%s__', $spec->getShortAlias());
       }
       if ($spec->getParamName()) {
         $param = $this->format(' __%s__', $spec->getParamName());
         $name .= $param;
         if ($short) {
           $short .= $param;
         }
       }
       $out[] = $name.$short;
       $out[] = $this->indent(10, $spec->getHelp());
       $out[] = null;
     }
 
     return implode("\n", $out);
   }
 
   private function format($str /* , ... */) {
     $args = func_get_args();
     return call_user_func_array(
       'phutil_console_format',
       $args);
   }
 
   private function indent($level, $str /* , ... */) {
     $args = func_get_args();
     $args = array_slice($args, 1);
     $text = call_user_func_array(array($this, 'format'), $args);
     return phutil_console_wrap($text, $level);
   }
 
   /**
    * @phutil-external-symbol function xhprof_disable
    */
   public function shutdownProfiler() {
     $data = xhprof_disable();
     $data = json_encode($data);
     Filesystem::writeFile($this->getArg('xprofile'), $data);
   }
 
 }
diff --git a/src/parser/argument/workflow/PhutilArgumentWorkflow.php b/src/parser/argument/workflow/PhutilArgumentWorkflow.php
index d994de1..98e4d95 100644
--- a/src/parser/argument/workflow/PhutilArgumentWorkflow.php
+++ b/src/parser/argument/workflow/PhutilArgumentWorkflow.php
@@ -1,181 +1,191 @@
 <?php
 
 /**
  * Used with @{class:PhutilArgumentParser} to build command line tools which
  * operate in several modes, called "workflows", like `git`, `svn`, `arc`,
  * `apt-get`. For example, you might build a simple calculator like this:
  *
  *   name=scripts/example/calculator.php
  *   $args = new PhutilArgumentParser($argv);
  *   $args->setTagline('simple calculator example');
  *   $args->setSynopsis(<<<EOHELP
  *   **calculator.php** __op__ __n__ ...
  *   Perform a calculation.
  *   EOHELP
  *   );
  *
  *   $add_workflow = id(new PhutilArgumentWorkflow())
  *     ->setName('add')
  *     ->setExamples('**add** __n__ ...')
  *     ->setSynopsis('Compute the sum of a list of numbers.')
  *     ->setArguments(
  *       array(
  *         array(
  *           'name'       => 'numbers',
  *           'wildcard'   => true,
  *         ),
  *       ));
  *
  *   $mul_workflow = id(new PhutilArgumentWorkflow())
  *     ->setName('mul')
  *     ->setExamples('**mul** __n__ ...')
  *     ->setSynopsis('Compute the product of a list of numbers.')
  *     ->setArguments(
  *       array(
  *         array(
  *           'name'       => 'numbers',
  *           'wildcard'   => true,
  *         ),
  *       ));
  *
  *   $flow = $args->parseWorkflows(
  *     array(
  *       $add_workflow,
  *       $mul_workflow,
  *       new PhutilHelpArgumentWorkflow(),
  *     ));
  *
  *   $nums = $args->getArg('numbers');
  *   if (empty($nums)) {
  *     echo "You must provide one or more numbers!\n";
  *     exit(1);
  *   }
  *
  *   foreach ($nums as $num) {
  *     if (!is_numeric($num)) {
  *       echo "Number '{$num}' is not numeric!\n";
  *       exit(1);
  *     }
  *   }
  *
  *   switch ($flow->getName()) {
  *     case 'add':
  *       echo array_sum($nums)."\n";
  *       break;
  *     case 'mul':
  *       echo array_product($nums)."\n";
  *       break;
  *   }
  *
  * You can also subclass this class and return `true` from
  * @{method:isExecutable}. In this case, the parser will automatically select
  * your workflow when the user invokes it.
  *
  * @concrete-extensible
  */
 class PhutilArgumentWorkflow extends Phobject {
 
   private $name;
   private $synopsis;
   private $specs = array();
   private $examples;
   private $help;
+  private $argv;
 
   final public function __construct() {
     $this->didConstruct();
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   /**
    * Provide brief usage examples of common calling conventions, like:
    *
    *   $workflow->setExamples("**delete** __file__ [__options__]");
    *
    * This text is shown in both brief and detailed help, and should give the
    * user a quick reference for common uses. You can separate several common
    * uses with newlines, but usually should not provide more than 2-3 examples.
    */
   final public function setExamples($examples) {
     $this->examples = $examples;
     return $this;
   }
 
   final public function getExamples() {
     if (!$this->examples) {
       return '**'.$this->name.'**';
     }
     return $this->examples;
   }
 
   /**
    * Provide a brief description of the command, like "Delete a file.".
    *
    * This text is shown in both brief and detailed help, and should give the
    * user a general idea of what the workflow does.
    */
   final public function setSynopsis($synopsis) {
     $this->synopsis = $synopsis;
     return $this;
   }
 
   final public function getSynopsis() {
     return $this->synopsis;
   }
 
 
   /**
    * Provide a full explanation of the command. This text is shown only in
    * detailed help.
    */
   final public function getHelp() {
     return $this->help;
   }
 
   final public function setHelp($help) {
     $this->help = $help;
     return $this;
   }
 
   final public function setArguments(array $specs) {
     $specs = PhutilArgumentSpecification::newSpecsFromList($specs);
     $this->specs = $specs;
     return $this;
   }
 
   final public function getArguments() {
     return $this->specs;
   }
 
+  final public function setArgv(PhutilArgumentParser $argv) {
+    $this->argv = $argv;
+    return $this;
+  }
+
+  final public function getArgv() {
+    return $this->argv;
+  }
+
   protected function didConstruct() {
     return null;
   }
 
   public function isExecutable() {
     return false;
   }
 
   public function execute(PhutilArgumentParser $args) {
     throw new Exception(pht("This workflow isn't executable!"));
   }
 
   /**
    * Normally, workflow arguments are parsed fully, so unexpected arguments will
    * raise an error. You can return `true` from this method to parse workflow
    * arguments only partially. This will allow you to manually parse remaining
    * arguments or delegate to a second level of workflows.
    *
    * @return bool True to partially parse workflow arguments (default false).
    */
   public function shouldParsePartial() {
     return false;
   }
 
 }