diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -9,8 +9,8 @@
   'names' => array(
     'conpherence.pkg.css' => '0e3cf785',
     'conpherence.pkg.js' => '020aebcf',
-    'core.pkg.css' => '0ae696de',
-    'core.pkg.js' => '68f29322',
+    'core.pkg.css' => '00a2e7f4',
+    'core.pkg.js' => 'd2de90d9',
     'dark-console.pkg.js' => '187792c2',
     'differential.pkg.css' => 'ffb69e3d',
     'differential.pkg.js' => '8deec4cd',
@@ -171,7 +171,7 @@
     'rsrc/css/phui/phui-invisible-character-view.css' => 'c694c4a4',
     'rsrc/css/phui/phui-left-right.css' => '68513c34',
     'rsrc/css/phui/phui-lightbox.css' => '4ebf22da',
-    'rsrc/css/phui/phui-list.css' => '2f253c22',
+    'rsrc/css/phui/phui-list.css' => '0c04affd',
     'rsrc/css/phui/phui-object-box.css' => 'b8d7eea0',
     'rsrc/css/phui/phui-pager.css' => 'd022c7ad',
     'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8',
@@ -246,7 +246,7 @@
     'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => 'a9f35511',
     'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '3a1b81f6',
     'rsrc/externals/javelin/lib/Cookie.js' => '05d290ef',
-    'rsrc/externals/javelin/lib/DOM.js' => '94681e22',
+    'rsrc/externals/javelin/lib/DOM.js' => 'e4c7622a',
     'rsrc/externals/javelin/lib/History.js' => '030b4f7a',
     'rsrc/externals/javelin/lib/JSON.js' => '541f81c3',
     'rsrc/externals/javelin/lib/Leader.js' => '0d2490ce',
@@ -717,7 +717,7 @@
     'javelin-color' => '78f811c9',
     'javelin-cookie' => '05d290ef',
     'javelin-diffusion-locate-file-source' => '94243d89',
-    'javelin-dom' => '94681e22',
+    'javelin-dom' => 'e4c7622a',
     'javelin-dynval' => '202a2e85',
     'javelin-event' => 'c03f2fb4',
     'javelin-external-editor-link-engine' => '48a8641f',
@@ -872,7 +872,7 @@
     'phui-invisible-character-view-css' => 'c694c4a4',
     'phui-left-right-css' => '68513c34',
     'phui-lightbox-css' => '4ebf22da',
-    'phui-list-view-css' => '2f253c22',
+    'phui-list-view-css' => '0c04affd',
     'phui-object-box-css' => 'b8d7eea0',
     'phui-oi-big-ui-css' => 'fa74cc35',
     'phui-oi-color-css' => 'b517bfa0',
@@ -1229,6 +1229,13 @@
       'aphront-typeahead-control-css',
       'phui-tag-view-css',
     ),
+    '36821f8d' => array(
+      'javelin-behavior',
+      'javelin-util',
+      'javelin-dom',
+      'javelin-stratcom',
+      'javelin-vector',
+    ),
     '3829a3cf' => array(
       'javelin-behavior',
       'javelin-uri',
@@ -1774,13 +1781,6 @@
       'javelin-uri',
       'javelin-routable',
     ),
-    '94681e22' => array(
-      'javelin-magical-init',
-      'javelin-install',
-      'javelin-util',
-      'javelin-vector',
-      'javelin-stratcom',
-    ),
     '9623adc1' => array(
       'javelin-behavior',
       'javelin-stratcom',
@@ -2160,6 +2160,13 @@
       'javelin-dom',
       'phuix-dropdown-menu',
     ),
+    'e4c7622a' => array(
+      'javelin-magical-init',
+      'javelin-install',
+      'javelin-util',
+      'javelin-vector',
+      'javelin-stratcom',
+    ),
     'e5bdb730' => array(
       'javelin-behavior',
       'javelin-stratcom',
diff --git a/resources/sprite/manifest/login.json b/resources/sprite/manifest/login.json
--- a/resources/sprite/manifest/login.json
+++ b/resources/sprite/manifest/login.json
@@ -59,7 +59,7 @@
     "login-MediaWiki": {
       "name": "login-MediaWiki",
       "rule": ".login-MediaWiki",
-      "hash": "f1f0a9382434081a9a84e7584828c2dd"
+      "hash": "68eba44e85ea942ecf14d3c08992a2e2"
     },
     "login-PayPal": {
       "name": "login-PayPal",
diff --git a/resources/sql/autopatches/20210625.owners.01.authority.sql b/resources/sql/autopatches/20210625.owners.01.authority.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20210625.owners.01.authority.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_package
+  ADD authorityMode VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20210625.owners.02.authority-default.sql b/resources/sql/autopatches/20210625.owners.02.authority-default.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20210625.owners.02.authority-default.sql
@@ -0,0 +1,3 @@
+UPDATE {$NAMESPACE}_owners.owners_package
+  SET authorityMode = 'strong'
+  WHERE authorityMode = '';
diff --git a/resources/sql/autopatches/20210713.harborcommand.01.migrate.sql b/resources/sql/autopatches/20210713.harborcommand.01.migrate.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20210713.harborcommand.01.migrate.sql
@@ -0,0 +1,4 @@
+INSERT IGNORE INTO {$NAMESPACE}_harbormaster.harbormaster_buildmessage
+  (authorPHID, receiverPHID, type, isConsumed, dateCreated, dateModified)
+  SELECT authorPHID, targetPHID, command, 0, dateCreated, dateModified
+    FROM {$NAMESPACE}_harbormaster.harbormaster_buildcommand;
diff --git a/resources/sql/autopatches/20210713.harborcommand.02.drop.sql b/resources/sql/autopatches/20210713.harborcommand.02.drop.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20210713.harborcommand.02.drop.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS {$NAMESPACE}_harbormaster.harbormaster_buildcommand;
diff --git a/resources/sql/autopatches/20210715.harborcommand.01.xactions.php b/resources/sql/autopatches/20210715.harborcommand.01.xactions.php
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20210715.harborcommand.01.xactions.php
@@ -0,0 +1,34 @@
+<?php
+
+// See T13072. Turn the old "process a command" transaction into modular
+// transactions that each handle one particular type of command.
+
+$xactions_table = new HarbormasterBuildTransaction();
+$xactions_conn = $xactions_table->establishConnection('w');
+$row_iterator = new LiskRawMigrationIterator(
+  $xactions_conn,
+  $xactions_table->getTableName());
+
+$map = array(
+  '"pause"' => 'message/pause',
+  '"abort"' => 'message/abort',
+  '"resume"' => 'message/resume',
+  '"restart"' => 'message/restart',
+);
+
+foreach ($row_iterator as $row) {
+  if ($row['transactionType'] !== 'harbormaster:build:command') {
+    continue;
+  }
+
+  $raw_value = $row['newValue'];
+
+  if (isset($map[$raw_value])) {
+    queryfx(
+      $xactions_conn,
+      'UPDATE %R SET transactionType = %s WHERE id = %d',
+      $xactions_table,
+      $map[$raw_value],
+      $row['id']);
+  }
+}
diff --git a/resources/sql/patches/20131004.dxreviewers.php b/resources/sql/patches/20131004.dxreviewers.php
--- a/resources/sql/patches/20131004.dxreviewers.php
+++ b/resources/sql/patches/20131004.dxreviewers.php
@@ -29,8 +29,7 @@
   foreach ($reviewer_phids as $dst) {
     if (phid_get_type($dst) == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
       // At least one old install ran into some issues here. Skip the row if we
-      // can't figure out what the destination PHID is. See here:
-      // https://github.com/phacility/phabricator/pull/507
+      // can't figure out what the destination PHID is.
       continue;
     }
 
diff --git a/scripts/setup/manage_celerity.php b/scripts/setup/manage_celerity.php
--- a/scripts/setup/manage_celerity.php
+++ b/scripts/setup/manage_celerity.php
@@ -2,7 +2,7 @@
 <?php
 
 $root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
+require_once $root.'/scripts/init/init-setup.php';
 
 $args = new PhutilArgumentParser($argv);
 $args->setTagline(pht('manage celerity'));
diff --git a/scripts/sql/manage_storage.php b/scripts/sql/manage_storage.php
--- a/scripts/sql/manage_storage.php
+++ b/scripts/sql/manage_storage.php
@@ -95,7 +95,7 @@
 
 $host = $args->getArg('host');
 $ref_key = $args->getArg('ref');
-if (strlen($host) || strlen($ref_key)) {
+if (($host !== null) || ($ref_key !== null)) {
   if ($host && $ref_key) {
     throw new PhutilArgumentUsageException(
       pht(
diff --git a/scripts/ssh/ssh-connect.php b/scripts/ssh/ssh-connect.php
--- a/scripts/ssh/ssh-connect.php
+++ b/scripts/ssh/ssh-connect.php
@@ -154,6 +154,6 @@
 array_unshift($arguments, $pattern);
 
 $err = newv('PhutilExecPassthru', $arguments)
-  ->execute();
+  ->resolve();
 
 exit($err);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -88,6 +88,7 @@
     'AlmanacInterfaceSearchEngine' => 'applications/almanac/query/AlmanacInterfaceSearchEngine.php',
     'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
     'AlmanacInterfaceTransaction' => 'applications/almanac/storage/AlmanacInterfaceTransaction.php',
+    'AlmanacInterfaceTransactionQuery' => 'applications/almanac/query/AlmanacInterfaceTransactionQuery.php',
     'AlmanacInterfaceTransactionType' => 'applications/almanac/xaction/AlmanacInterfaceTransactionType.php',
     'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
     'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php',
@@ -338,6 +339,7 @@
     'ChatLogConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogConduitAPIMethod.php',
     'ChatLogQueryConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php',
     'ChatLogRecordConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php',
+    'ConduitAPIDocumentationPage' => 'applications/conduit/data/ConduitAPIDocumentationPage.php',
     'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php',
     'ConduitAPIMethodTestCase' => 'applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php',
     'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php',
@@ -904,6 +906,7 @@
     'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
     'DiffusionMercurialBlameQuery' => 'applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php',
     'DiffusionMercurialCommandEngine' => 'applications/diffusion/protocol/DiffusionMercurialCommandEngine.php',
+    'DiffusionMercurialCommandEngineTests' => 'applications/diffusion/protocol/__tests__/DiffusionMercurialCommandEngineTests.php',
     'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
     'DiffusionMercurialFlagInjectionException' => 'applications/diffusion/exception/DiffusionMercurialFlagInjectionException.php',
     'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
@@ -1386,8 +1389,9 @@
     'HarbormasterBuildArtifactPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php',
     'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
     'HarbormasterBuildAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php',
-    'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
     'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php',
+    'HarbormasterBuildEditAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildEditAPIMethod.php',
+    'HarbormasterBuildEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildEditEngine.php',
     'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
     'HarbormasterBuildFailureException' => 'applications/harbormaster/exception/HarbormasterBuildFailureException.php',
     'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php',
@@ -1407,7 +1411,12 @@
     'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
     'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
     'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
+    'HarbormasterBuildMessageAbortTransaction' => 'applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php',
+    'HarbormasterBuildMessagePauseTransaction' => 'applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php',
     'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
+    'HarbormasterBuildMessageRestartTransaction' => 'applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php',
+    'HarbormasterBuildMessageResumeTransaction' => 'applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php',
+    'HarbormasterBuildMessageTransaction' => 'applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php',
     'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
     'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
     'HarbormasterBuildPlanBehavior' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php',
@@ -1458,6 +1467,7 @@
     'HarbormasterBuildTransaction' => 'applications/harbormaster/storage/HarbormasterBuildTransaction.php',
     'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php',
     'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php',
+    'HarbormasterBuildTransactionType' => 'applications/harbormaster/xaction/build/HarbormasterBuildTransactionType.php',
     'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php',
     'HarbormasterBuildUnitMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php',
     'HarbormasterBuildView' => 'applications/harbormaster/view/HarbormasterBuildView.php',
@@ -1466,9 +1476,12 @@
     'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
     'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php',
     'HarbormasterBuildableAdapterInterface' => 'applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php',
+    'HarbormasterBuildableEditAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildableEditAPIMethod.php',
+    'HarbormasterBuildableEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildableEditEngine.php',
     'HarbormasterBuildableEngine' => 'applications/harbormaster/engine/HarbormasterBuildableEngine.php',
     'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php',
     'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php',
+    'HarbormasterBuildableMessageTransaction' => 'applications/harbormaster/xaction/buildable/HarbormasterBuildableMessageTransaction.php',
     'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php',
     'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php',
     'HarbormasterBuildableSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildableSearchAPIMethod.php',
@@ -1477,14 +1490,15 @@
     'HarbormasterBuildableTransaction' => 'applications/harbormaster/storage/HarbormasterBuildableTransaction.php',
     'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php',
     'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
+    'HarbormasterBuildableTransactionType' => 'applications/harbormaster/xaction/buildable/HarbormasterBuildableTransactionType.php',
     'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
-    'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/integration/buildkite/HarbormasterBuildkiteBuildStepImplementation.php',
+    'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php',
     'HarbormasterBuildkiteBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildkiteBuildableInterface.php',
-    'HarbormasterBuildkiteHookHandler' => 'applications/harbormaster/integration/buildkite/HarbormasterBuildkiteHookHandler.php',
+    'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php',
     'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php',
-    'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/integration/circleci/HarbormasterCircleCIBuildStepImplementation.php',
+    'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php',
     'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php',
-    'HarbormasterCircleCIHookHandler' => 'applications/harbormaster/integration/circleci/HarbormasterCircleCIHookHandler.php',
+    'HarbormasterCircleCIHookController' => 'applications/harbormaster/controller/HarbormasterCircleCIHookController.php',
     'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
     'HarbormasterControlBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php',
     'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
@@ -1498,8 +1512,6 @@
     'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php',
     'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
-    'HarbormasterHookController' => 'applications/harbormaster/controller/HarbormasterHookController.php',
-    'HarbormasterHookHandler' => 'applications/harbormaster/integration/HarbormasterHookHandler.php',
     'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php',
     'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php',
     'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
@@ -1513,6 +1525,7 @@
     'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
     'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
     'HarbormasterManagementWriteLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php',
+    'HarbormasterMessageException' => 'applications/harbormaster/exception/HarbormasterMessageException.php',
     'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
     'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
     'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
@@ -1530,7 +1543,6 @@
     'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php',
     'HarbormasterQueryBuildsSearchEngineAttachment' => 'applications/harbormaster/engineextension/HarbormasterQueryBuildsSearchEngineAttachment.php',
     'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php',
-    'HarbormasterRestartException' => 'applications/harbormaster/exception/HarbormasterRestartException.php',
     'HarbormasterRunBuildPlansHeraldAction' => 'applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php',
     'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php',
     'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php',
@@ -3973,6 +3985,7 @@
     'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php',
     'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php',
     'PhabricatorOwnersPackageAuditingTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php',
+    'PhabricatorOwnersPackageAuthorityTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAuthorityTransaction.php',
     'PhabricatorOwnersPackageAutoreviewTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php',
     'PhabricatorOwnersPackageContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPackageContextFreeGrammar.php',
     'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php',
@@ -5763,6 +5776,7 @@
     'PhutilRemarkupEngine' => 'infrastructure/markup/remarkup/PhutilRemarkupEngine.php',
     'PhutilRemarkupEngineTestCase' => 'infrastructure/markup/remarkup/__tests__/PhutilRemarkupEngineTestCase.php',
     'PhutilRemarkupEscapeRemarkupRule' => 'infrastructure/markup/markuprule/PhutilRemarkupEscapeRemarkupRule.php',
+    'PhutilRemarkupEvalRule' => 'infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php',
     'PhutilRemarkupHeaderBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupHeaderBlockRule.php',
     'PhutilRemarkupHighlightRule' => 'infrastructure/markup/markuprule/PhutilRemarkupHighlightRule.php',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
@@ -6134,6 +6148,7 @@
     'AlmanacInterfaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'AlmanacInterfaceTableView' => 'AphrontView',
     'AlmanacInterfaceTransaction' => 'AlmanacModularTransaction',
+    'AlmanacInterfaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
     'AlmanacInterfaceTransactionType' => 'AlmanacTransactionType',
     'AlmanacKeys' => 'Phobject',
     'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability',
@@ -6424,6 +6439,7 @@
     'ChatLogConduitAPIMethod' => 'ConduitAPIMethod',
     'ChatLogQueryConduitAPIMethod' => 'ChatLogConduitAPIMethod',
     'ChatLogRecordConduitAPIMethod' => 'ChatLogConduitAPIMethod',
+    'ConduitAPIDocumentationPage' => 'Phobject',
     'ConduitAPIMethod' => array(
       'Phobject',
       'PhabricatorPolicyInterface',
@@ -7047,6 +7063,7 @@
     'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
     'DiffusionMercurialBlameQuery' => 'DiffusionBlameQuery',
     'DiffusionMercurialCommandEngine' => 'DiffusionCommandEngine',
+    'DiffusionMercurialCommandEngineTests' => 'PhabricatorTestCase',
     'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionMercurialFlagInjectionException' => 'Exception',
     'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
@@ -7601,8 +7618,9 @@
     'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildAutoplan' => 'Phobject',
-    'HarbormasterBuildCommand' => 'HarbormasterDAO',
     'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource',
+    'HarbormasterBuildEditAPIMethod' => 'PhabricatorEditEngineAPIMethod',
+    'HarbormasterBuildEditEngine' => 'PhabricatorEditEngine',
     'HarbormasterBuildEngine' => 'Phobject',
     'HarbormasterBuildFailureException' => 'Exception',
     'HarbormasterBuildGraph' => 'AbstractDirectedGraph',
@@ -7631,7 +7649,12 @@
       'PhabricatorPolicyInterface',
       'PhabricatorDestructibleInterface',
     ),
+    'HarbormasterBuildMessageAbortTransaction' => 'HarbormasterBuildMessageTransaction',
+    'HarbormasterBuildMessagePauseTransaction' => 'HarbormasterBuildMessageTransaction',
     'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'HarbormasterBuildMessageRestartTransaction' => 'HarbormasterBuildMessageTransaction',
+    'HarbormasterBuildMessageResumeTransaction' => 'HarbormasterBuildMessageTransaction',
+    'HarbormasterBuildMessageTransaction' => 'HarbormasterBuildTransactionType',
     'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildPlan' => array(
       'HarbormasterDAO',
@@ -7702,9 +7725,10 @@
     'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildTargetSearchEngine' => 'PhabricatorApplicationSearchEngine',
-    'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction',
+    'HarbormasterBuildTransaction' => 'PhabricatorModularTransaction',
     'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'HarbormasterBuildTransactionType' => 'PhabricatorModularTransactionType',
     'HarbormasterBuildUnitMessage' => array(
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
@@ -7722,22 +7746,26 @@
       'PhabricatorDestructibleInterface',
     ),
     'HarbormasterBuildableActionController' => 'HarbormasterController',
+    'HarbormasterBuildableEditAPIMethod' => 'PhabricatorEditEngineAPIMethod',
+    'HarbormasterBuildableEditEngine' => 'PhabricatorEditEngine',
     'HarbormasterBuildableEngine' => 'Phobject',
     'HarbormasterBuildableListController' => 'HarbormasterController',
+    'HarbormasterBuildableMessageTransaction' => 'HarbormasterBuildableTransactionType',
     'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildableSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
     'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'HarbormasterBuildableStatus' => 'Phobject',
-    'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction',
+    'HarbormasterBuildableTransaction' => 'PhabricatorModularTransaction',
     'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'HarbormasterBuildableTransactionType' => 'PhabricatorModularTransactionType',
     'HarbormasterBuildableViewController' => 'HarbormasterController',
     'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
-    'HarbormasterBuildkiteHookHandler' => 'HarbormasterHookHandler',
+    'HarbormasterBuildkiteHookController' => 'HarbormasterController',
     'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup',
     'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
-    'HarbormasterCircleCIHookHandler' => 'HarbormasterHookHandler',
+    'HarbormasterCircleCIHookController' => 'HarbormasterController',
     'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
     'HarbormasterControlBuildStepGroup' => 'HarbormasterBuildStepGroup',
     'HarbormasterController' => 'PhabricatorController',
@@ -7751,8 +7779,6 @@
     'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup',
     'HarbormasterFileArtifact' => 'HarbormasterArtifact',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
-    'HarbormasterHookController' => 'HarbormasterController',
-    'HarbormasterHookHandler' => 'Phobject',
     'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact',
     'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterLintMessagesController' => 'HarbormasterController',
@@ -7766,6 +7792,7 @@
     'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
     'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
     'HarbormasterManagementWriteLogWorkflow' => 'HarbormasterManagementWorkflow',
+    'HarbormasterMessageException' => 'Exception',
     'HarbormasterMessageType' => 'Phobject',
     'HarbormasterObject' => 'HarbormasterDAO',
     'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
@@ -7783,7 +7810,6 @@
     'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterQueryBuildsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
     'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule',
-    'HarbormasterRestartException' => 'Exception',
     'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction',
     'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HarbormasterScratchTable' => 'HarbormasterDAO',
@@ -10598,6 +10624,7 @@
       'PhabricatorNgramsInterface',
     ),
     'PhabricatorOwnersPackageAuditingTransaction' => 'PhabricatorOwnersPackageTransactionType',
+    'PhabricatorOwnersPackageAuthorityTransaction' => 'PhabricatorOwnersPackageTransactionType',
     'PhabricatorOwnersPackageAutoreviewTransaction' => 'PhabricatorOwnersPackageTransactionType',
     'PhabricatorOwnersPackageContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource',
@@ -12758,6 +12785,7 @@
     'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
     'PhutilRemarkupEngineTestCase' => 'PhutilTestCase',
     'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule',
+    'PhutilRemarkupEvalRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule',
diff --git a/src/applications/almanac/query/AlmanacInterfaceTransactionQuery.php b/src/applications/almanac/query/AlmanacInterfaceTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/almanac/query/AlmanacInterfaceTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class AlmanacInterfaceTransactionQuery
+  extends PhabricatorApplicationTransactionQuery {
+
+  public function getTemplateApplicationTransaction() {
+    return new AlmanacInterfaceTransaction();
+  }
+
+}
diff --git a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php
--- a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php
+++ b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php
@@ -30,7 +30,7 @@
 
       'abc' => true,
       'a.b' => true,
-      'db.phacility.instance' => true,
+      'db.companyname.instance' => true,
       'web002.useast.example.com' => true,
       'master.example-corp.com' => true,
 
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -252,7 +252,7 @@
 
     $message = pht(
       'ERROR: You are making a Conduit API request to "%s", but the correct '.
-      'HTTP request path to use in order to access a COnduit method is "%s" '.
+      'HTTP request path to use in order to access a Conduit method is "%s" '.
       '(for example, "%s"). Check your configuration.',
       $request_path,
       $conduit_path,
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -135,10 +135,9 @@
 
 
   /**
-   * Returns true if an application is first-party (developed by Phacility)
-   * and false otherwise.
+   * Returns true if an application is first-party and false otherwise.
    *
-   * @return bool True if this application is developed by Phacility.
+   * @return bool True if this application is first-party.
    */
   final public function isFirstParty() {
     $where = id(new ReflectionClass($this))->getFileName();
diff --git a/src/applications/calendar/parser/ics/PhutilICSWriter.php b/src/applications/calendar/parser/ics/PhutilICSWriter.php
--- a/src/applications/calendar/parser/ics/PhutilICSWriter.php
+++ b/src/applications/calendar/parser/ics/PhutilICSWriter.php
@@ -128,11 +128,15 @@
 
     $properties[] = $this->newTextProperty(
       'PRODID',
-      '-//Phacility//Phabricator//EN');
+      self::getICSPRODID());
 
     return $properties;
   }
 
+  public static function getICSPRODID() {
+    return '-//Phacility//Phabricator//EN';
+  }
+
   private function getEventNodeProperties(PhutilCalendarEventNode $event) {
     $properties = array();
 
diff --git a/src/applications/calendar/parser/ics/__tests__/PhutilICSWriterTestCase.php b/src/applications/calendar/parser/ics/__tests__/PhutilICSWriterTestCase.php
--- a/src/applications/calendar/parser/ics/__tests__/PhutilICSWriterTestCase.php
+++ b/src/applications/calendar/parser/ics/__tests__/PhutilICSWriterTestCase.php
@@ -138,6 +138,12 @@
   private function assertICS($name, $actual) {
     $path = dirname(__FILE__).'/data/'.$name;
     $data = Filesystem::readFile($path);
+
+    $data = str_replace(
+      '${PRODID}',
+      PhutilICSWriter::getICSPRODID(),
+      $data);
+
     $this->assertEqual($data, $actual, pht('ICS: %s', $name));
   }
 
diff --git a/src/applications/calendar/parser/ics/__tests__/data/writer-christmas.ics b/src/applications/calendar/parser/ics/__tests__/data/writer-christmas.ics
--- a/src/applications/calendar/parser/ics/__tests__/data/writer-christmas.ics
+++ b/src/applications/calendar/parser/ics/__tests__/data/writer-christmas.ics
@@ -1,6 +1,6 @@
 BEGIN:VCALENDAR
 VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
+PRODID:${PRODID}
 BEGIN:VEVENT
 UID:christmas-day
 CREATED:20160901T232425Z
diff --git a/src/applications/calendar/parser/ics/__tests__/data/writer-office-party.ics b/src/applications/calendar/parser/ics/__tests__/data/writer-office-party.ics
--- a/src/applications/calendar/parser/ics/__tests__/data/writer-office-party.ics
+++ b/src/applications/calendar/parser/ics/__tests__/data/writer-office-party.ics
@@ -1,6 +1,6 @@
 BEGIN:VCALENDAR
 VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
+PRODID:${PRODID}
 BEGIN:VEVENT
 UID:office-party
 CREATED:20161001T120000Z
diff --git a/src/applications/calendar/parser/ics/__tests__/data/writer-recurring-christmas.ics b/src/applications/calendar/parser/ics/__tests__/data/writer-recurring-christmas.ics
--- a/src/applications/calendar/parser/ics/__tests__/data/writer-recurring-christmas.ics
+++ b/src/applications/calendar/parser/ics/__tests__/data/writer-recurring-christmas.ics
@@ -1,6 +1,6 @@
 BEGIN:VCALENDAR
 VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
+PRODID:${PRODID}
 BEGIN:VEVENT
 UID:recurring-christmas
 CREATED:20001225T000000Z
diff --git a/src/applications/calendar/parser/ics/__tests__/data/writer-tea-time.ics b/src/applications/calendar/parser/ics/__tests__/data/writer-tea-time.ics
--- a/src/applications/calendar/parser/ics/__tests__/data/writer-tea-time.ics
+++ b/src/applications/calendar/parser/ics/__tests__/data/writer-tea-time.ics
@@ -1,6 +1,6 @@
 BEGIN:VCALENDAR
 VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
+PRODID:${PRODID}
 BEGIN:VEVENT
 UID:tea-time
 CREATED:20160915T070000Z
diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
--- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
@@ -88,23 +88,118 @@
     $crumbs->addTextCrumb($method->getAPIMethodName());
     $crumbs->setBorder(true);
 
+    $documentation_pages = $method->getDocumentationPages($viewer);
+
+    $documentation_view = $this->newDocumentationView(
+      $method,
+      $documentation_pages);
+
     $view = id(new PHUITwoColumnView())
       ->setHeader($header)
       ->setFooter(array(
+
+        id(new PhabricatorAnchorView())
+          ->setAnchorName('overview'),
         $info_box,
-        $method->getMethodDocumentation(),
+
+        id(new PhabricatorAnchorView())
+          ->setAnchorName('documentation'),
+        $documentation_view,
+
+        id(new PhabricatorAnchorView())
+          ->setAnchorName('call'),
         $form_box,
+
+        id(new PhabricatorAnchorView())
+          ->setAnchorName('examples'),
         $this->renderExampleBox($method, null),
       ));
 
     $title = $method->getAPIMethodName();
 
+    $nav = $this->newNavigationView($method, $documentation_pages);
+
     return $this->newPage()
       ->setTitle($title)
       ->setCrumbs($crumbs)
+      ->setNavigation($nav)
       ->appendChild($view);
   }
 
+  private function newDocumentationView(
+    ConduitAPIMethod $method,
+    array $documentation_pages) {
+    assert_instances_of($documentation_pages, 'ConduitAPIDocumentationPage');
+
+    $viewer = $this->getViewer();
+
+    $description_properties = id(new PHUIPropertyListView());
+
+    $description_properties->addTextContent(
+      new PHUIRemarkupView($viewer, $method->getMethodDescription()));
+
+    $description_box = id(new PHUIObjectBoxView())
+      ->setHeaderText(pht('Method Description'))
+      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+      ->appendChild($description_properties);
+
+    $view = array();
+    $view[] = $description_box;
+
+    foreach ($documentation_pages as $page) {
+      $view[] = $page->newView();
+    }
+
+    return $view;
+  }
+
+  private function newNavigationView(
+    ConduitAPIMethod $method,
+    array $documentation_pages) {
+    assert_instances_of($documentation_pages, 'ConduitAPIDocumentationPage');
+
+    $console_uri = urisprintf(
+      '/method/%s/',
+      $method->getAPIMethodName());
+    $console_uri = $this->getApplicationURI($console_uri);
+    $console_uri = new PhutilURI($console_uri);
+
+    $nav = id(new AphrontSideNavFilterView())
+      ->setBaseURI($console_uri);
+
+    $nav->selectFilter(null);
+
+    $nav->newLink('overview')
+      ->setHref('#overview')
+      ->setName(pht('Overview'))
+      ->setIcon('fa-list');
+
+    $nav->newLink('documentation')
+      ->setHref('#documentation')
+      ->setName(pht('Documentation'))
+      ->setIcon('fa-book');
+
+    foreach ($documentation_pages as $page) {
+      $nav->newLink($page->getAnchor())
+        ->setHref('#'.$page->getAnchor())
+        ->setName($page->getName())
+        ->setIcon($page->getIconIcon())
+        ->setIndented(true);
+    }
+
+    $nav->newLink('call')
+      ->setHref('#call')
+      ->setName(pht('Call Method'))
+      ->setIcon('fa-play');
+
+    $nav->newLink('examples')
+      ->setHref('#examples')
+      ->setName(pht('Examples'))
+      ->setIcon('fa-folder-open-o');
+
+    return $nav;
+  }
+
   private function buildMethodProperties(ConduitAPIMethod $method) {
     $viewer = $this->getViewer();
 
@@ -171,7 +266,6 @@
       pht('Errors'),
       $error_description);
 
-
     $scope = $method->getRequiredScope();
     switch ($scope) {
       case ConduitAPIMethod::SCOPE_ALWAYS:
@@ -201,11 +295,6 @@
         $oauth_description,
       ));
 
-    $view->addSectionHeader(
-      pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
-    $view->addTextContent(
-      new PHUIRemarkupView($viewer, $method->getMethodDescription()));
-
     return $view;
   }
 
diff --git a/src/applications/conduit/data/ConduitAPIDocumentationPage.php b/src/applications/conduit/data/ConduitAPIDocumentationPage.php
new file mode 100644
--- /dev/null
+++ b/src/applications/conduit/data/ConduitAPIDocumentationPage.php
@@ -0,0 +1,61 @@
+<?php
+
+final class ConduitAPIDocumentationPage
+  extends Phobject {
+
+  private $name;
+  private $anchor;
+  private $iconIcon;
+  private $content = array();
+
+  public function setName($name) {
+    $this->name = $name;
+    return $this;
+  }
+
+  public function getName() {
+    return $this->name;
+  }
+
+  public function setAnchor($anchor) {
+    $this->anchor = $anchor;
+    return $this;
+  }
+
+  public function getAnchor() {
+    return $this->anchor;
+  }
+
+  public function setContent($content) {
+    $this->content = $content;
+    return $this;
+  }
+
+  public function getContent() {
+    return $this->content;
+  }
+
+  public function setIconIcon($icon_icon) {
+    $this->iconIcon = $icon_icon;
+    return $this;
+  }
+
+  public function getIconIcon() {
+    return $this->iconIcon;
+  }
+
+  public function newView() {
+    $anchor_name = $this->getAnchor();
+    $anchor_view = id(new PhabricatorAnchorView())
+      ->setAnchorName($anchor_name);
+
+    $content = $this->content;
+
+    return array(
+      $anchor_view,
+      $content,
+    );
+  }
+
+
+}
diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php
--- a/src/applications/conduit/method/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitAPIMethod.php
@@ -40,8 +40,33 @@
    */
   abstract public function getMethodDescription();
 
-  public function getMethodDocumentation() {
-    return null;
+  final public function getDocumentationPages(PhabricatorUser $viewer) {
+    $pages = $this->newDocumentationPages($viewer);
+    return $pages;
+  }
+
+  protected function newDocumentationPages(PhabricatorUser $viewer) {
+    return array();
+  }
+
+  final protected function newDocumentationPage(PhabricatorUser $viewer) {
+    return id(new ConduitAPIDocumentationPage())
+      ->setIconIcon('fa-chevron-right');
+  }
+
+  final protected function newDocumentationBoxPage(
+    PhabricatorUser $viewer,
+    $title,
+    $content) {
+
+    $box_view = id(new PHUIObjectBoxView())
+      ->setHeaderText($title)
+      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+      ->setTable($content);
+
+    return $this->newDocumentationPage($viewer)
+      ->setName($title)
+      ->setContent($box_view);
   }
 
   abstract protected function defineParamTypes();
diff --git a/src/applications/config/check/PhabricatorBinariesSetupCheck.php b/src/applications/config/check/PhabricatorBinariesSetupCheck.php
--- a/src/applications/config/check/PhabricatorBinariesSetupCheck.php
+++ b/src/applications/config/check/PhabricatorBinariesSetupCheck.php
@@ -120,17 +120,11 @@
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
           $bad_versions = array(
-            // We need 1.9 for HTTP cloning, see T3046.
-            '< 1.9' => pht(
-              'The minimum supported version of Mercurial is 1.9, which was '.
-              'released in 2011.'),
-            '= 2.1' => pht(
-              'This version of Mercurial returns a bad exit code '.
-              'after a successful pull.'),
-            '= 2.2' => pht(
-              'This version of Mercurial has a significant memory leak, fixed '.
-              'in 2.2.1. Pushing fails with this version as well; see %s.',
-              'T3046#54922'),
+            // We need 2.4 for utilizing `{p1node}` keyword in templates, see
+            // D21679 and D21681.
+            '< 2.4' => pht(
+              'The minimum supported version of Mercurial is 2.4, which was '.
+              'released in 2012.'),
           );
           break;
       }
diff --git a/src/applications/config/controller/PhabricatorConfigConsoleController.php b/src/applications/config/controller/PhabricatorConfigConsoleController.php
--- a/src/applications/config/controller/PhabricatorConfigConsoleController.php
+++ b/src/applications/config/controller/PhabricatorConfigConsoleController.php
@@ -56,7 +56,7 @@
       ->setBorder(true);
 
     $box = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Phabricator Configuation'))
+      ->setHeaderText(pht('Phabricator Configuration'))
       ->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
       ->setObjectList($menu);
 
@@ -72,7 +72,7 @@
       ->setFooter($launcher_view);
 
     return $this->newPage()
-      ->setTitle(pht('Phabricator Configuation'))
+      ->setTitle(pht('Phabricator Configuration'))
       ->setCrumbs($crumbs)
       ->appendChild($view);
   }
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -1282,7 +1282,7 @@
   }
 
   private function buildUnitMessagesView(
-    $diff,
+    DifferentialDiff $diff,
     DifferentialRevision $revision) {
     $viewer = $this->getViewer();
 
@@ -1310,14 +1310,8 @@
       return null;
     }
 
-    $excuse = null;
-    if ($diff->hasDiffProperty('arc:unit-excuse')) {
-      $excuse = $diff->getProperty('arc:unit-excuse');
-    }
-
     return id(new HarbormasterUnitSummaryView())
       ->setViewer($viewer)
-      ->setExcuse($excuse)
       ->setBuildable($diff->getBuildable())
       ->setUnitMessages($diff->getUnitMessages())
       ->setLimit(5)
diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -42,7 +42,7 @@
   private $unsavedChangesets = array();
   private $changesets = self::ATTACHABLE;
   private $revision = self::ATTACHABLE;
-  private $properties = array();
+  private $properties = self::ATTACHABLE;
   private $buildable = self::ATTACHABLE;
 
   private $unitMessages = self::ATTACHABLE;
@@ -338,6 +338,9 @@
   }
 
   public function attachProperty($key, $value) {
+    if (!is_array($this->properties)) {
+      $this->properties = array();
+    }
     $this->properties[$key] = $value;
     return $this;
   }
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -311,9 +311,17 @@
     // which the actor may be able to use their authority over to gain the
     // ability to force-accept for other packages. This query doesn't apply
     // dominion rules yet, and we'll bypass those rules later on.
+
+    // See T13657. We ignore "watcher" packages which don't grant their owners
+    // permission to force accept anything.
+
     $authority_query = id(new PhabricatorOwnersPackageQuery())
       ->setViewer($viewer)
       ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
+      ->withAuthorityModes(
+        array(
+          PhabricatorOwnersPackage::AUTHORITY_STRONG,
+        ))
       ->withAuthorityPHIDs(array($viewer->getPHID()))
       ->withControl($repository_phid, $paths);
     $authority_packages = $authority_query->execute();
diff --git a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
--- a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
@@ -136,28 +136,33 @@
     // stop history (this is more consistent with the Mercurial worldview of
     // branches).
 
+    $path_args = array();
     if (strlen($path)) {
-      $path_arg = csprintf('%s', $path);
+      $path_args[] = $path;
       $revset_arg = hgsprintf(
         'reverse(ancestors(%s))',
         $commit_hash);
     } else {
-      $path_arg = '';
       $revset_arg = hgsprintf(
         'reverse(ancestors(%s)) and branch(%s)',
-        $drequest->getBranch(),
-        $commit_hash);
+        $commit_hash,
+        $drequest->getBranch());
+    }
+
+    $hg_analyzer = PhutilBinaryAnalyzer::getForBinary('hg');
+    if ($hg_analyzer->isMercurialTemplatePnodeAvailable()) {
+      $hg_log_template = '{node} {p1.node} {p2.node}\\n';
+    } else {
+      $hg_log_template = '{node} {p1node} {p2node}\\n';
     }
 
     list($stdout) = $repository->execxLocalCommand(
-      'log --debug --template %s --limit %d --rev %s -- %C',
-      '{node};{parents}\\n',
+      'log --template %s --limit %d --rev %s -- %Ls',
+      $hg_log_template,
       ($offset + $limit), // No '--skip' in Mercurial.
       $revset_arg,
-      $path_arg);
+      $path_args);
 
-    $stdout = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
-      $stdout);
     $lines = explode("\n", trim($stdout));
     $lines = array_slice($lines, $offset);
 
@@ -166,28 +171,19 @@
 
     $last = null;
     foreach (array_reverse($lines) as $line) {
-      list($hash, $parents) = explode(';', $line);
-      $parents = trim($parents);
-      if (!$parents) {
-        if ($last === null) {
-          $parent_map[$hash] = array('...');
-        } else {
-          $parent_map[$hash] = array($last);
-        }
-      } else {
-        $parents = preg_split('/\s+/', $parents);
-        foreach ($parents as $parent) {
-          list($plocal, $phash) = explode(':', $parent);
-          if (!preg_match('/^0+$/', $phash)) {
-            $parent_map[$hash][] = $phash;
-          }
-        }
-        // This may happen for the zeroth commit in repository, both hashes
-        // are "000000000...".
-        if (empty($parent_map[$hash])) {
-          $parent_map[$hash] = array('...');
+      $parts = explode(' ', trim($line));
+      $hash = $parts[0];
+      $parents = array_slice($parts, 1, 2);
+      foreach ($parents as $parent) {
+        if (!preg_match('/^0+\z/', $parent)) {
+          $parent_map[$hash][] = $parent;
         }
       }
+      // This may happen for the zeroth commit in repository, both hashes
+      // are "000000000...".
+      if (empty($parent_map[$hash])) {
+        $parent_map[$hash] = array('...');
+      }
 
       // The rendering code expects the first commit to be "mainline", like
       // Git. Flip the order so it does the right thing.
diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php
--- a/src/applications/diffusion/controller/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseController.php
@@ -298,22 +298,8 @@
       $empty_result->setDiffusionBrowseResultSet($results);
       $empty_result->setView($request->getStr('view'));
     } else {
-      $phids = array();
-      foreach ($results->getPaths() as $result) {
-        $data = $result->getLastCommitData();
-        if ($data) {
-          if ($data->getCommitDetail('authorPHID')) {
-            $phids[$data->getCommitDetail('authorPHID')] = true;
-          }
-        }
-      }
-
-      $phids = array_keys($phids);
-      $handles = $this->loadViewerHandles($phids);
-
       $browse_table = id(new DiffusionBrowseTableView())
         ->setDiffusionRequest($drequest)
-        ->setHandles($handles)
         ->setPaths($results->getPaths())
         ->setUser($request->getUser());
 
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php
--- a/src/applications/diffusion/controller/DiffusionRepositoryController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php
@@ -2,7 +2,6 @@
 
 final class DiffusionRepositoryController extends DiffusionController {
 
-  private $historyFuture;
   private $browseFuture;
   private $branchButton = null;
   private $branchFuture;
@@ -191,15 +190,6 @@
     $path = $drequest->getPath();
 
     $futures = array();
-    $this->historyFuture = $this->callConduitMethod(
-      'diffusion.historyquery',
-      array(
-        'commit' => $commit,
-        'path' => $path,
-        'offset' => 0,
-        'limit' => 15,
-      ));
-    $futures[] = $this->historyFuture;
 
     $browse_pager = id(new PHUIPagerView())
       ->readFromRequest($request);
@@ -230,32 +220,8 @@
       // Just resolve all the futures before continuing.
     }
 
-    $phids = array();
     $content = array();
 
-    try {
-      $history_results = $this->historyFuture->resolve();
-      $history = DiffusionPathChange::newFromConduit(
-        $history_results['pathChanges']);
-
-      foreach ($history as $item) {
-        $data = $item->getCommitData();
-        if ($data) {
-          if ($data->getCommitDetail('authorPHID')) {
-            $phids[$data->getCommitDetail('authorPHID')] = true;
-          }
-          if ($data->getCommitDetail('committerPHID')) {
-            $phids[$data->getCommitDetail('committerPHID')] = true;
-          }
-        }
-      }
-      $history_exception = null;
-    } catch (Exception $ex) {
-      $history_results = null;
-      $history = null;
-      $history_exception = $ex;
-    }
-
     try {
       $browse_results = $this->browseFuture->resolve();
       $browse_results = DiffusionBrowseResultSet::newFromConduit(
@@ -264,18 +230,6 @@
       $browse_paths = $browse_results->getPaths();
       $browse_paths = $browse_pager->sliceResults($browse_paths);
 
-      foreach ($browse_paths as $item) {
-        $data = $item->getLastCommitData();
-        if ($data) {
-          if ($data->getCommitDetail('authorPHID')) {
-            $phids[$data->getCommitDetail('authorPHID')] = true;
-          }
-          if ($data->getCommitDetail('committerPHID')) {
-            $phids[$data->getCommitDetail('committerPHID')] = true;
-          }
-        }
-      }
-
       $browse_exception = null;
     } catch (Exception $ex) {
       $browse_results = null;
@@ -283,9 +237,6 @@
       $browse_exception = $ex;
     }
 
-    $phids = array_keys($phids);
-    $handles = $this->loadViewerHandles($phids);
-
     if ($browse_results) {
       $readme = $this->renderDirectoryReadme($browse_results);
     } else {
@@ -296,7 +247,6 @@
       $browse_results,
       $browse_paths,
       $browse_exception,
-      $handles,
       $browse_pager);
 
     if ($readme) {
@@ -524,7 +474,6 @@
     $browse_results,
     $browse_paths,
     $browse_exception,
-    array $handles,
     PHUIPagerView $pager) {
 
     require_celerity_resource('diffusion-icons-css');
@@ -547,8 +496,7 @@
 
     $browse_table = id(new DiffusionBrowseTableView())
       ->setUser($viewer)
-      ->setDiffusionRequest($drequest)
-      ->setHandles($handles);
+      ->setDiffusionRequest($drequest);
     if ($browse_paths) {
       $browse_table->setPaths($browse_paths);
     } else {
diff --git a/src/applications/diffusion/protocol/DiffusionMercurialCommandEngine.php b/src/applications/diffusion/protocol/DiffusionMercurialCommandEngine.php
--- a/src/applications/diffusion/protocol/DiffusionMercurialCommandEngine.php
+++ b/src/applications/diffusion/protocol/DiffusionMercurialCommandEngine.php
@@ -71,12 +71,36 @@
     //
     // Separately, it may fail to write to a different branch cache, and may
     // encounter issues reading the branch cache.
+    //
+    // When Mercurial repositories are hosted on external systems with
+    // multi-user environments it's possible that the branch cache is computed
+    // on a revision which does not end up being published. When this happens it
+    // will recompute the cache but also print out "invalid branch cache".
+    //
+    // https://www.mercurial-scm.org/pipermail/mercurial/2014-June/047239.html
+    //
+    // When observing a repository which uses largefiles, the debug output may
+    // also contain extraneous output about largefile changes.
+    //
+    // At some point Mercurial added/improved support for pager used when
+    // command output is large. It includes printing out debug information that
+    // the pager is being started for a command. This seems to happen despite
+    // the output of the command being piped/read from another process.
+    //
+    // When printing color output Mercurial may run into some issue with the
+    // terminal info. This should never happen in Phabricator since color
+    // output should be turned off, however in the event it shows up we should
+    // filter it out anyways.
 
     $ignore = array(
       'ignoring untrusted configuration option',
       "couldn't write revision branch cache:",
       "couldn't write branch cache:",
       'invalid branchheads cache',
+      'invalid branch cache',
+      'updated patterns: .hglf',
+      'starting pager for command',
+      'no terminfo entry for',
     );
 
     foreach ($ignore as $key => $pattern) {
diff --git a/src/applications/diffusion/protocol/__tests__/DiffusionMercurialCommandEngineTests.php b/src/applications/diffusion/protocol/__tests__/DiffusionMercurialCommandEngineTests.php
new file mode 100644
--- /dev/null
+++ b/src/applications/diffusion/protocol/__tests__/DiffusionMercurialCommandEngineTests.php
@@ -0,0 +1,89 @@
+<?php
+
+final class DiffusionMercurialCommandEngineTests extends PhabricatorTestCase {
+
+  public function testFilteringDebugOutput() {
+    $map = array(
+      '' => '',
+
+      "quack\n" => "quack\n",
+
+      "ignoring untrusted configuration option x.y = z\nquack\n" =>
+        "quack\n",
+
+      "ignoring untrusted configuration option x.y = z\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "quack\n" =>
+        "quack\n",
+
+      "ignoring untrusted configuration option x.y = z\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "quack\n" =>
+        "quack\n",
+
+      "quack\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "ignoring untrusted configuration option x.y = z\n" =>
+        "quack\n",
+
+      "ignoring untrusted configuration option x.y = z\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "duck\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "bread\n".
+      "ignoring untrusted configuration option x.y = z\n".
+      "quack\n" =>
+        "duck\nbread\nquack\n",
+
+      "ignoring untrusted configuration option x.y = z\n".
+      "duckignoring untrusted configuration option x.y = z\n".
+      "quack" =>
+        'duckquack',
+    );
+
+    foreach ($map as $input => $expect) {
+      $actual = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
+        $input);
+      $this->assertEqual($expect, $actual, $input);
+    }
+
+    // Output that should be filtered out from the results
+    $output =
+      "ignoring untrusted configuration option\n".
+      "couldn't write revision branch cache:\n".
+      "couldn't write branch cache: blah blah blah\n".
+      "invalid branchheads cache\n".
+      "invalid branch cache (served): tip differs\n".
+      "starting pager for command 'log'\n".
+      "updated patterns: ".
+        ".hglf/project/src/a/b/c/SomeClass.java, ".
+        "project/src/a/b/c/SomeClass.java\n".
+      "no terminfo entry for sitm\n";
+
+    $filtered_output =
+      DiffusionMercurialCommandEngine::filterMercurialDebugOutput($output);
+
+    $this->assertEqual('', $filtered_output);
+
+    // The output that should make it through the filtering
+    $output =
+      "0b33a9e5ceedba14b03214f743957357d7bb46a9;694".
+        ":8b39f63eb209dd2bdfd4bd3d0721a9e38d75a6d3".
+        "-1:0000000000000000000000000000000000000000\n".
+      "8b39f63eb209dd2bdfd4bd3d0721a9e38d75a6d3;693".
+        ":165bce9ce4ccc97024ba19ed5a22f6a066fa6844".
+        "-1:0000000000000000000000000000000000000000\n".
+      "165bce9ce4ccc97024ba19ed5a22f6a066fa6844;692:".
+        "2337bc9e3cf212b3b386b5197801b1c81db64920".
+        "-1:0000000000000000000000000000000000000000\n";
+
+    $filtered_output =
+      DiffusionMercurialCommandEngine::filterMercurialDebugOutput($output);
+
+    $this->assertEqual($output, $filtered_output);
+  }
+
+}
diff --git a/src/applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php b/src/applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php
--- a/src/applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php
+++ b/src/applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php
@@ -6,11 +6,26 @@
     $repository = $request->getRepository();
     $commit = $request->getCommit();
 
-    // NOTE: We're using "--debug" to make "--changeset" give us the full
-    // commit hashes.
+    // NOTE: Using "--template" or "--debug" to get the full commit hashes.
+    $hg_analyzer = PhutilBinaryAnalyzer::getForBinary('hg');
+    if ($hg_analyzer->isMercurialAnnotateTemplatesAvailable()) {
+      // See `hg help annotate --verbose` for more info on the template format.
+      // Use array of arguments so the template line does not need wrapped in
+      // quotes.
+      $template = array(
+        '--template',
+        "{lines % '{node}: {line}'}",
+      );
+    } else {
+      $template = array(
+        '--debug',
+        '--changeset',
+      );
+    }
 
     return $repository->getLocalCommandFuture(
-      'annotate --debug --changeset --rev %s -- %s',
+      'annotate %Ls --rev %s -- %s',
+      $template,
       $commit,
       $path);
   }
@@ -26,6 +41,21 @@
 
     $lines = phutil_split_lines($stdout);
     foreach ($lines as $line) {
+      // If the `--debug` flag was used above instead of `--template` then
+      // there's a good change additional output was included which is not
+      // relevant to the information we want. It should be safe to call this
+      // regardless of whether we used `--debug` or `--template` so there isn't
+      // a need to track which argument was used.
+      $line = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
+        $line);
+
+      // Just in case new versions of Mercurial add arbitrary output when using
+      // the `--debug`, do a quick sanity check that this line is formatted in
+      // a way we're expecting.
+      if (strpos($line, ':') === false) {
+        phlog(pht('Unexpected output from hg annotate: %s', $line));
+        continue;
+      }
       list($commit) = explode(':', $line, 2);
       $result[] = $commit;
     }
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
@@ -47,23 +47,23 @@
   private function loadMercurialParents() {
     $repository = $this->getRepository();
 
+    $hg_analyzer = PhutilBinaryAnalyzer::getForBinary('hg');
+    if ($hg_analyzer->isMercurialTemplatePnodeAvailable()) {
+      $hg_log_template = '{p1.node} {p2.node}';
+    } else {
+      $hg_log_template = '{p1node} {p2node}';
+    }
+
     list($stdout) = $repository->execxLocalCommand(
-      'log --debug --limit 1 --template={parents} --rev %s',
+      'log --limit 1 --template %s --rev %s',
+      $hg_log_template,
       $this->identifier);
 
-    $stdout = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
-      $stdout);
-
     $hashes = preg_split('/\s+/', trim($stdout));
     foreach ($hashes as $key => $value) {
-      // Mercurial parents look like "23:ad9f769d6f786fad9f76d9a" -- we want
-      // to strip out the local rev part.
-      list($local, $global) = explode(':', $value);
-      $hashes[$key] = $global;
-
-      // With --debug we get 40-character hashes but also get the "000000..."
-      // hash for missing parents; ignore it.
-      if (preg_match('/^0+$/', $global)) {
+      // We get 40-character hashes but also get the "000000..." hash for
+      // missing parents; ignore it.
+      if (preg_match('/^0+\z/', $value)) {
         unset($hashes[$key]);
       }
     }
diff --git a/src/applications/diffusion/view/DiffusionBrowseTableView.php b/src/applications/diffusion/view/DiffusionBrowseTableView.php
--- a/src/applications/diffusion/view/DiffusionBrowseTableView.php
+++ b/src/applications/diffusion/view/DiffusionBrowseTableView.php
@@ -3,7 +3,6 @@
 final class DiffusionBrowseTableView extends DiffusionView {
 
   private $paths;
-  private $handles = array();
 
   public function setPaths(array $paths) {
     assert_instances_of($paths, 'DiffusionRepositoryPath');
@@ -11,12 +10,6 @@
     return $this;
   }
 
-  public function setHandles(array $handles) {
-    assert_instances_of($handles, 'PhabricatorObjectHandle');
-    $this->handles = $handles;
-    return $this;
-  }
-
   public function render() {
     $request = $this->getDiffusionRequest();
     $repository = $request->getRepository();
@@ -29,7 +22,6 @@
 
     $need_pull = array();
     $rows = array();
-    $show_edit = false;
     foreach ($this->paths as $path) {
       $full_path = $base_path.$path->getPath();
 
diff --git a/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php b/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
--- a/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
@@ -26,6 +26,7 @@
 
   public function canWriteFiles() {
     $path = PhabricatorEnv::getEnvConfig('storage.local-disk.path');
+    $path = phutil_string_cast($path);
     return (bool)strlen($path);
   }
 
diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
--- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
+++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
@@ -94,7 +94,10 @@
         'lint/' => array(
           '(?P<id>\d+)/' => 'HarbormasterLintMessagesController',
         ),
-        'hook/(?P<handler>[^/]+)/' => 'HarbormasterHookController',
+        'hook/' => array(
+          'circleci/' => 'HarbormasterCircleCIHookController',
+          'buildkite/' => 'HarbormasterBuildkiteHookController',
+        ),
         'log/' => array(
           'view/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
             => 'HarbormasterBuildLogViewController',
diff --git a/src/applications/harbormaster/conduit/HarbormasterBuildEditAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterBuildEditAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/conduit/HarbormasterBuildEditAPIMethod.php
@@ -0,0 +1,20 @@
+<?php
+
+final class HarbormasterBuildEditAPIMethod
+  extends PhabricatorEditEngineAPIMethod {
+
+  public function getAPIMethodName() {
+    return 'harbormaster.build.edit';
+  }
+
+  public function newEditEngine() {
+    return new HarbormasterBuildEditEngine();
+  }
+
+  public function getMethodSummary() {
+    return pht(
+      'Apply transactions to create a new build or edit an existing '.
+      'one.');
+  }
+
+}
diff --git a/src/applications/harbormaster/conduit/HarbormasterBuildableEditAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterBuildableEditAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/conduit/HarbormasterBuildableEditAPIMethod.php
@@ -0,0 +1,20 @@
+<?php
+
+final class HarbormasterBuildableEditAPIMethod
+  extends PhabricatorEditEngineAPIMethod {
+
+  public function getAPIMethodName() {
+    return 'harbormaster.buildable.edit';
+  }
+
+  public function newEditEngine() {
+    return new HarbormasterBuildableEditEngine();
+  }
+
+  public function getMethodSummary() {
+    return pht(
+      'Apply transactions to create a new buildable or edit an existing '.
+      'one.');
+  }
+
+}
diff --git a/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
--- a/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
+++ b/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
@@ -7,14 +7,6 @@
       'PhabricatorHarbormasterApplication');
   }
 
-  public function getMethodStatus() {
-    return self::METHOD_STATUS_UNSTABLE;
-  }
-
-  public function getMethodStatusDescription() {
-    return pht('All Harbormaster APIs are new and subject to change.');
-  }
-
   protected function returnArtifactList(array $artifacts) {
     $list = array();
 
diff --git a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php
--- a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php
+++ b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php
@@ -9,18 +9,222 @@
 
   public function getMethodSummary() {
     return pht(
-      'Send a message about the status of a build target to Harbormaster, '.
-      'notifying the application of build results in an external system.');
+      'Modify running builds, and report build results.');
   }
 
   public function getMethodDescription() {
+    return pht(<<<EOREMARKUP
+Pause, abort, restart, and report results for builds.
+EOREMARKUP
+      );
+  }
+
+  protected function newDocumentationPages(PhabricatorUser $viewer) {
+    $pages = array();
+
+    $pages[] = $this->newSendingDocumentationBoxPage($viewer);
+    $pages[] = $this->newBuildsDocumentationBoxPage($viewer);
+    $pages[] = $this->newCommandsDocumentationBoxPage($viewer);
+    $pages[] = $this->newTargetsDocumentationBoxPage($viewer);
+    $pages[] = $this->newUnitDocumentationBoxPage($viewer);
+    $pages[] = $this->newLintDocumentationBoxPage($viewer);
+
+    return $pages;
+  }
+
+  private function newSendingDocumentationBoxPage(PhabricatorUser $viewer) {
+    $title = pht('Sending Messages');
+    $content = pht(<<<EOREMARKUP
+Harbormaster build objects work somewhat differently from objects in many other
+applications. Most application objects can be edited directly using synchronous
+APIs (like `maniphest.edit`, `differential.revision.edit`, and so on).
+
+However, builds require long-running background processing and Habormaster
+objects have a more complex lifecycle than most other application objects and
+may spend significant periods of time locked by daemon processes during build
+execition. A synchronous edit might need to wait an arbitrarily long amount of
+time for this lock to become available so the edit could be applied.
+
+Additionally, some edits may also require an arbitrarily long amount of time to
+//complete//. For example, aborting a build may execute cleanup steps which
+take minutes (or even hours) to complete.
+
+Since a synchronous API could not guarantee it could return results to the
+caller in a reasonable amount of time, the edit API for Harbormaster build
+objects is asynchronous: to update a Harbormaster build or build target, use
+this API (`harbormaster.sendmessage`) to send it a message describing an edit
+you would like to effect or additional information you want to provide.
+The message will be processed by the daemons once the build or target reaches
+a suitable state to receive messages.
+
+Select an object to send a message to using the `receiver` parameter. This
+API method can send messages to multiple types of objects:
+
+<table>
+  <tr>
+    <th>Object Type</th>
+    <th>PHID Example</th>
+    <th>Description</th>
+  </tr>
+  <tr>
+    <td>Harbormaster Buildable</td>
+    <td>`PHID-HMBB-...`</td>
+    <td>%s</td>
+  </tr>
+  <tr>
+    <td>Harbormaster Build</td>
+    <td>`PHID-HMBD-...`</td>
+    <td>%s</td>
+  </tr>
+  <tr>
+    <td>Harbormaster Build Target</td>
+    <td>`PHID-HMBT-...`</td>
+    <td>%s</td>
+  </tr>
+</table>
+
+See below for specifics on sending messages to different object types.
+EOREMARKUP
+      ,
+      pht(
+        'Buildables may receive control commands like "abort" and "restart". '.
+        'Sending a control command to a Buildable is the same as sending it '.
+        'to each Build for the Buildable.'),
+      pht(
+        'Builds may receive control commands like "pause", "resume", "abort", '.
+        'and "restart".'),
+      pht(
+        'Build Targets may receive build status and result messages, like '.
+        '"pass" or "fail".'));
+
+    $content = $this->newRemarkupDocumentationView($content);
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('sending')
+      ->setIconIcon('fa-envelope-o');
+  }
+
+  private function newBuildsDocumentationBoxPage(PhabricatorUser $viewer) {
+    $title = pht('Updating Builds');
+
+    $content = pht(<<<EOREMARKUP
+You can use this method (`harbormaster.sendmessage`) to send control commands
+to Buildables and Builds.
+
+Specify the Build or Buildable to receive the control command by providing its
+PHID in the `receiver` parameter.
+
+Sending a control command to a Buildable has the same effect as sending it to
+each Build for the Buildable. For example, sending a "Pause" message to a
+Buildable will pause all builds for the Buildable (or at least attempt to).
+
+When sending control commands, the `unit` and `lint` parameters of this API
+method must be omitted. You can not report lint or unit results directly to
+a Build or Buildable, and can not report them alongside a control command.
+
+More broadly, you can not report build results directly to a Build or
+Buildable. Instead, report results to a Build Target.
+
+See below for a list of control commands.
+
+EOREMARKUP
+      );
+
+    $content = $this->newRemarkupDocumentationView($content);
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('builds')
+      ->setIconIcon('fa-cubes');
+  }
+
+  private function newCommandsDocumentationBoxPage(PhabricatorUser $viewer) {
+    $messages = HarbormasterBuildMessageTransaction::getAllMessages();
+
+    $rows = array();
+
+    $rows[] = '<tr>';
+    $rows[] = '<th>'.pht('Message Type').'</th>';
+    $rows[] = '<th>'.pht('Description').'</th>';
+    $rows[] = '</tr>';
+
+    foreach ($messages as $message) {
+      $row = array();
+
+      $row[] = sprintf(
+        '<td>`%s`</td>',
+        $message->getHarbormasterBuildMessageType());
+
+      $row[] = sprintf(
+        '<td>%s</td>',
+        $message->getHarbormasterBuildMessageDescription());
+
+      $rows[] = sprintf(
+        '<tr>%s</tr>',
+        implode("\n", $row));
+    }
+
+    $message_table = sprintf(
+      '<table>%s</table>',
+      implode("\n", $rows));
+
+    $title = pht('Control Commands');
+
+    $content = pht(<<<EOREMARKUP
+You can use this method to send control commands to Buildables and Builds.
+
+This table summarizes which object types may receive control commands:
+
+<table>
+  <tr>
+    <th>Object Type</th>
+    <th>PHID Example</th>
+    <th />
+    <th>Description</th>
+  </tr>
+  <tr>
+    <td>Harbormaster Buildable</td>
+    <td>`PHID-HMBB-...`</td>
+    <td>{icon check color=green}</td>
+    <td>Buildables may receive control commands.</td>
+  </tr>
+  <tr>
+    <td>Harbormaster Build</td>
+    <td>`PHID-HMBD-...`</td>
+    <td>{icon check color=green}</td>
+    <td>Builds may receive control commands.</td>
+  </tr>
+  <tr>
+    <td>Harbormaster Build Target</td>
+    <td>`PHID-HMBT-...`</td>
+    <td>{icon times color=red}</td>
+    <td>You may **NOT** send control commands to build targets.</td>
+  </tr>
+</table>
+
+You can send these commands:
+
+%s
+
+To send a command message, specify the PHID of the object you would like to
+receive the message using the `receiver` parameter, and specify the message
+type using the `type` parameter.
+
+EOREMARKUP
+      ,
+      $message_table);
+
+    $content = $this->newRemarkupDocumentationView($content);
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('commands')
+      ->setIconIcon('fa-exclamation-triangle');
+  }
+
+  private function newTargetsDocumentationBoxPage(PhabricatorUser $viewer) {
     $messages = HarbormasterMessageType::getAllMessages();
 
-    $head_type = pht('Constant');
-    $head_desc = pht('Description');
-    $head_key = pht('Key');
     $head_type = pht('Type');
-    $head_name = pht('Name');
+    $head_desc = pht('Description');
 
     $rows = array();
     $rows[] = "| {$head_type} | {$head_desc} |";
@@ -31,6 +235,84 @@
     }
     $message_table = implode("\n", $rows);
 
+    $content = pht(<<<EOREMARKUP
+If you run external builds, you can use this method to publish build results
+back into Harbormaster after the external system finishes work (or as it makes
+progress).
+
+To report build status or results, you must send a message to the appropriate
+Build Target. This table summarizes which object types may receive build status
+and result messages:
+
+<table>
+  <tr>
+    <th>Object Type</th>
+    <th>PHID Example</th>
+    <th />
+    <th>Description</th>
+  </tr>
+  <tr>
+    <td>Harbormaster Buildable</td>
+    <td>`PHID-HMBB-...`</td>
+    <td>{icon times color=red}</td>
+    <td>Buildables may **NOT** receive status or result messages.</td>
+  </tr>
+  <tr>
+    <td>Harbormaster Build</td>
+    <td>`PHID-HMBD-...`</td>
+    <td>{icon times color=red}</td>
+    <td>Builds may **NOT** receive status or result messages.</td>
+  </tr>
+  <tr>
+    <td>Harbormaster Build Target</td>
+    <td>`PHID-HMBT-...`</td>
+    <td>{icon check color=green}</td>
+    <td>Report build status and results to Build Targets.</td>
+  </tr>
+</table>
+
+The simplest way to use this method to report build results is to call it once
+after the build finishes with a `pass` or `fail` message. This will record the
+build result, and continue the next step in the build if the build was waiting
+for a result.
+
+When you send a status message about a build target, you can optionally include
+detailed `lint` or `unit` results alongside the message. See below for details.
+
+If you want to report intermediate results but a build hasn't completed yet,
+you can use the `work` message. This message doesn't have any direct effects,
+but allows you to send additional data to update the progress of the build
+target. The target will continue waiting for a completion message, but the UI
+will update to show the progress which has been made.
+
+When sending a message to a build target to report the status or results of
+a build, your message must include a `type` which describes the overall state
+of the build. For example, use `pass` to tell Harbormaster that a build target
+completed successfully.
+
+Supported message types are:
+
+%s
+
+EOREMARKUP
+      ,
+      $message_table);
+
+    $title = pht('Updating Build Targets');
+
+    $content = $this->newRemarkupDocumentationView($content);
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('targets')
+      ->setIconIcon('fa-bullseye');
+  }
+
+  private function newUnitDocumentationBoxPage(PhabricatorUser $viewer) {
+    $head_key = pht('Key');
+    $head_desc = pht('Description');
+    $head_name = pht('Name');
+    $head_type = pht('Type');
+
     $rows = array();
     $rows[] = "| {$head_key} | {$head_type} | {$head_desc} |";
     $rows[] = '|-------------|--------------|--------------|';
@@ -55,6 +337,64 @@
     }
     $result_table = implode("\n", $rows);
 
+    $valid_unit = array(
+      array(
+        'name' => 'PassingTest',
+        'result' => ArcanistUnitTestResult::RESULT_PASS,
+      ),
+      array(
+        'name' => 'FailingTest',
+        'result' => ArcanistUnitTestResult::RESULT_FAIL,
+      ),
+    );
+
+    $json = new PhutilJSON();
+    $valid_unit = $json->encodeAsList($valid_unit);
+
+
+    $title = pht('Reporting Unit Results');
+
+    $content = pht(<<<EOREMARKUP
+You can report test results when updating the state of a build target. The
+simplest way to do this is to report all the results alongside a `pass` or
+`fail` message, but you can also send a `work` message to report intermediate
+results.
+
+
+To provide unit test results, pass a list of results in the `unit`
+parameter. Each result should be a dictionary with these keys:
+
+%s
+
+The `result` parameter recognizes these test results:
+
+%s
+
+This is a simple, valid value for the `unit` parameter. It reports one passing
+test and one failing test:
+
+```lang=json
+%s
+```
+EOREMARKUP
+      ,
+      $unit_table,
+      $result_table,
+      $valid_unit);
+
+    $content = $this->newRemarkupDocumentationView($content);
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('unit');
+  }
+
+  private function newLintDocumentationBoxPage(PhabricatorUser $viewer) {
+
+    $head_key = pht('Key');
+    $head_desc = pht('Description');
+    $head_name = pht('Name');
+    $head_type = pht('Type');
+
     $rows = array();
     $rows[] = "| {$head_key} | {$head_type} | {$head_desc} |";
     $rows[] = '|-------------|--------------|--------------|';
@@ -76,17 +416,6 @@
     }
     $severity_table = implode("\n", $rows);
 
-    $valid_unit = array(
-      array(
-        'name' => 'PassingTest',
-        'result' => ArcanistUnitTestResult::RESULT_PASS,
-      ),
-      array(
-        'name' => 'FailingTest',
-        'result' => ArcanistUnitTestResult::RESULT_FAIL,
-      ),
-    );
-
     $valid_lint = array(
       array(
         'name' => pht('Syntax Error'),
@@ -109,104 +438,58 @@
     );
 
     $json = new PhutilJSON();
-    $valid_unit = $json->encodeAsList($valid_unit);
     $valid_lint = $json->encodeAsList($valid_lint);
 
-    return pht(
-      "Send a message about the status of a build target to Harbormaster, ".
-      "notifying the application of build results in an external system.".
-      "\n\n".
-      "Sending Messages\n".
-      "================\n".
-      "If you run external builds, you can use this method to publish build ".
-      "results back into Harbormaster after the external system finishes work ".
-      "or as it makes progress.".
-      "\n\n".
-      "The simplest way to use this method is to call it once after the ".
-      "build finishes with a `pass` or `fail` message. This will record the ".
-      "build result, and continue the next step in the build if the build was ".
-      "waiting for a result.".
-      "\n\n".
-      "When you send a status message about a build target, you can ".
-      "optionally include detailed `lint` or `unit` results alongside the ".
-      "message. See below for details.".
-      "\n\n".
-      "If you want to report intermediate results but a build hasn't ".
-      "completed yet, you can use the `work` message. This message doesn't ".
-      "have any direct effects, but allows you to send additional data to ".
-      "update the progress of the build target. The target will continue ".
-      "waiting for a completion message, but the UI will update to show the ".
-      "progress which has been made.".
-      "\n\n".
-      "Message Types\n".
-      "=============\n".
-      "When you send Harbormaster a message, you must include a `type`, ".
-      "which describes the overall state of the build. For example, use ".
-      "`pass` to tell Harbormaster that a build completed successfully.".
-      "\n\n".
-      "Supported message types are:".
-      "\n\n".
-      "%s".
-      "\n\n".
-      "Unit Results\n".
-      "============\n".
-      "You can report test results alongside a message. The simplest way to ".
-      "do this is to report all the results alongside a `pass` or `fail` ".
-      "message, but you can also send a `work` message to report intermediate ".
-      "results.\n\n".
-      "To provide unit test results, pass a list of results in the `unit` ".
-      "parameter. Each result should be a dictionary with these keys:".
-      "\n\n".
-      "%s".
-      "\n\n".
-      "The `result` parameter recognizes these test results:".
-      "\n\n".
-      "%s".
-      "\n\n".
-      "This is a simple, valid value for the `unit` parameter. It reports ".
-      "one passing test and one failing test:\n\n".
-      "\n\n".
-      "```lang=json\n".
-      "%s".
-      "```".
-      "\n\n".
-      "Lint Results\n".
-      "============\n".
-      "Like unit test results, you can report lint results alongside a ".
-      "message. The `lint` parameter should contain results as a list of ".
-      "dictionaries with these keys:".
-      "\n\n".
-      "%s".
-      "\n\n".
-      "The `severity` parameter recognizes these severity levels:".
-      "\n\n".
-      "%s".
-      "\n\n".
-      "This is a simple, valid value for the `lint` parameter. It reports one ".
-      "error and one warning:".
-      "\n\n".
-      "```lang=json\n".
-      "%s".
-      "```".
-      "\n\n",
-      $message_table,
-      $unit_table,
-      $result_table,
-      $valid_unit,
+    $title = pht('Reporting Lint Results');
+    $content = pht(<<<EOREMARKUP
+Like unit test results, you can report lint results when updating the state
+of a build target. The `lint` parameter should contain results as a list of
+dictionaries with these keys:
+
+%s
+
+The `severity` parameter recognizes these severity levels:
+
+%s
+
+This is a simple, valid value for the `lint` parameter. It reports one error
+and one warning:
+
+```lang=json
+%s
+```
+
+EOREMARKUP
+      ,
       $lint_table,
       $severity_table,
       $valid_lint);
+
+    $content = $this->newRemarkupDocumentationView($content);
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('lint');
   }
 
   protected function defineParamTypes() {
     $messages = HarbormasterMessageType::getAllMessages();
+
+    $more_messages = HarbormasterBuildMessageTransaction::getAllMessages();
+    $more_messages = mpull($more_messages, 'getHarbormasterBuildMessageType');
+
+    $messages = array_merge($messages, $more_messages);
+    $messages = array_unique($messages);
+
+    sort($messages);
+
     $type_const = $this->formatStringConstants($messages);
 
     return array(
-      'buildTargetPHID' => 'required phid',
+      'receiver' => 'required string|phid',
       'type' => 'required '.$type_const,
       'unit' => 'optional list<wild>',
       'lint' => 'optional list<wild>',
+      'buildTargetPHID' => 'deprecated optional phid',
     );
   }
 
@@ -215,19 +498,90 @@
   }
 
   protected function execute(ConduitAPIRequest $request) {
-    $viewer = $request->getUser();
+    $viewer = $request->getViewer();
+
+    $receiver_name = $request->getValue('receiver');
 
     $build_target_phid = $request->getValue('buildTargetPHID');
+    if ($build_target_phid !== null) {
+      if ($receiver_name === null) {
+        $receiver_name = $build_target_phid;
+      } else {
+        throw new Exception(
+          pht(
+            'Call specifies both "receiver" and "buildTargetPHID". '.
+            'When using the modern "receiver" parameter, omit the '.
+            'deprecated "buildTargetPHID" parameter.'));
+      }
+    }
+
+    if (!strlen($receiver_name)) {
+      throw new Exception(
+        pht(
+          'Call omits required "receiver" parameter. Specify the PHID '.
+          'of the object you want to send a message to.'));
+    }
+
     $message_type = $request->getValue('type');
+    if (!strlen($message_type)) {
+      throw new Exception(
+        pht(
+          'Call omits required "type" parameter. Specify the type of '.
+          'message you want to send.'));
+    }
 
-    $build_target = id(new HarbormasterBuildTargetQuery())
+    $receiver_object = id(new PhabricatorObjectQuery())
       ->setViewer($viewer)
-      ->withPHIDs(array($build_target_phid))
+      ->withNames(array($receiver_name))
       ->executeOne();
-    if (!$build_target) {
-      throw new Exception(pht('No such build target!'));
+    if (!$receiver_object) {
+      throw new Exception(
+        pht(
+          'Unable to load object "%s" to receive message.',
+          $receiver_name));
+    }
+
+    $is_target = ($receiver_object instanceof HarbormasterBuildTarget);
+    if ($is_target) {
+      return $this->sendToTarget($request, $message_type, $receiver_object);
+    }
+
+    if ($request->getValue('unit') !== null) {
+      throw new Exception(
+        pht(
+          'Call includes "unit" parameter. This parameter must be omitted '.
+          'when the receiver is not a Build Target.'));
+    }
+
+    if ($request->getValue('lint') !== null) {
+      throw new Exception(
+        pht(
+          'Call includes "lint" parameter. This parameter must be omitted '.
+          'when the receiver is not a Build Target.'));
+    }
+
+    $is_build = ($receiver_object instanceof HarbormasterBuild);
+    if ($is_build) {
+      return $this->sendToBuild($request, $message_type, $receiver_object);
+    }
+
+    $is_buildable = ($receiver_object instanceof HarbormasterBuildable);
+    if ($is_buildable) {
+      return $this->sendToBuildable($request, $message_type, $receiver_object);
     }
 
+    throw new Exception(
+      pht(
+        'Receiver object (of class "%s") is not a valid receiver.',
+        get_class($receiver_object)));
+  }
+
+  private function sendToTarget(
+    ConduitAPIRequest $request,
+    $message_type,
+    HarbormasterBuildTarget $build_target) {
+    $viewer = $request->getViewer();
+
     $save = array();
 
     $lint_messages = $request->getValue('lint', array());
@@ -270,4 +624,67 @@
     return null;
   }
 
+  private function sendToBuild(
+    ConduitAPIRequest $request,
+    $message_type,
+    HarbormasterBuild $build) {
+    $viewer = $request->getViewer();
+
+    $xaction =
+      HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
+        $message_type);
+    if (!$xaction) {
+      throw new Exception(
+        pht(
+          'Message type "%s" is not supported.',
+          $message_type));
+    }
+
+    // NOTE: This is a slightly weaker check than we perform in the web UI.
+    // We allow API callers to send a "pause" message to a pausing build,
+    // for example, even though the message will have no effect.
+    $xaction->assertCanApplyMessage($viewer, $build);
+
+    $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType());
+  }
+
+  private function sendToBuildable(
+    ConduitAPIRequest $request,
+    $message_type,
+    HarbormasterBuildable $buildable) {
+    $viewer = $request->getViewer();
+
+    $xaction =
+      HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
+        $message_type);
+    if (!$xaction) {
+      throw new Exception(
+        pht(
+          'Message type "%s" is not supported.',
+          $message_type));
+    }
+
+    // Reload the Buildable to load Builds.
+    $buildable = id(new HarbormasterBuildableQuery())
+      ->setViewer($viewer)
+      ->withIDs(array($buildable->getID()))
+      ->needBuilds(true)
+      ->executeOne();
+
+    $can_send = array();
+    foreach ($buildable->getBuilds() as $build) {
+      if ($xaction->canApplyMessage($viewer, $build)) {
+        $can_send[] = $build;
+      }
+    }
+
+    // NOTE: This doesn't actually apply a transaction to the Buildable,
+    // but that transaction is purely informational and should probably be
+    // implemented as a Message.
+
+    foreach ($can_send as $build) {
+      $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType());
+    }
+  }
+
 }
diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
--- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
+++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
@@ -12,6 +12,11 @@
   const STATUS_PAUSED = 'paused';
   const STATUS_DEADLOCKED = 'deadlocked';
 
+  const PENDING_PAUSING = 'x-pausing';
+  const PENDING_RESUMING = 'x-resuming';
+  const PENDING_RESTARTING = 'x-restarting';
+  const PENDING_ABORTING = 'x-aborting';
+
   private $key;
   private $properties;
 
@@ -56,6 +61,37 @@
     return ($this->key === self::STATUS_FAILED);
   }
 
+  public function isAborting() {
+    return ($this->key === self::PENDING_ABORTING);
+  }
+
+  public function isRestarting() {
+    return ($this->key === self::PENDING_RESTARTING);
+  }
+
+  public function isResuming() {
+    return ($this->key === self::PENDING_RESUMING);
+  }
+
+  public function isPausing() {
+    return ($this->key === self::PENDING_PAUSING);
+  }
+
+  public function isPending() {
+    return ($this->key === self::STATUS_PENDING);
+  }
+
+  public function getIconIcon() {
+    return $this->getProperty('icon');
+  }
+
+  public function getIconColor() {
+    return $this->getProperty('color');
+  }
+
+  public function getName() {
+    return $this->getProperty('name');
+  }
 
   /**
    * Get a human readable name for a build status constant.
@@ -185,8 +221,8 @@
       ),
       self::STATUS_PAUSED => array(
         'name' => pht('Paused'),
-        'icon' => 'fa-minus-circle',
-        'color' => 'dark',
+        'icon' => 'fa-pause',
+        'color' => 'yellow',
         'color.ansi' => 'yellow',
         'isBuilding' => false,
         'isComplete' => false,
@@ -199,6 +235,38 @@
         'isBuilding' => false,
         'isComplete' => true,
       ),
+      self::PENDING_PAUSING => array(
+        'name' => pht('Pausing'),
+        'icon' => 'fa-exclamation-triangle',
+        'color' => 'red',
+        'color.ansi' => 'red',
+        'isBuilding' => false,
+        'isComplete' => false,
+      ),
+      self::PENDING_RESUMING => array(
+        'name' => pht('Resuming'),
+        'icon' => 'fa-exclamation-triangle',
+        'color' => 'red',
+        'color.ansi' => 'red',
+        'isBuilding' => false,
+        'isComplete' => false,
+      ),
+      self::PENDING_RESTARTING => array(
+        'name' => pht('Restarting'),
+        'icon' => 'fa-exclamation-triangle',
+        'color' => 'red',
+        'color.ansi' => 'red',
+        'isBuilding' => false,
+        'isComplete' => false,
+      ),
+      self::PENDING_ABORTING => array(
+        'name' => pht('Aborting'),
+        'icon' => 'fa-exclamation-triangle',
+        'color' => 'red',
+        'color.ansi' => 'red',
+        'isBuilding' => false,
+        'isComplete' => false,
+      ),
     );
   }
 
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
--- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
@@ -22,24 +22,13 @@
       return new Aphront404Response();
     }
 
-    switch ($action) {
-      case HarbormasterBuildCommand::COMMAND_RESTART:
-        $can_issue = $build->canRestartBuild();
-        break;
-      case HarbormasterBuildCommand::COMMAND_PAUSE:
-        $can_issue = $build->canPauseBuild();
-        break;
-      case HarbormasterBuildCommand::COMMAND_RESUME:
-        $can_issue = $build->canResumeBuild();
-        break;
-      case HarbormasterBuildCommand::COMMAND_ABORT:
-        $can_issue = $build->canAbortBuild();
-        break;
-      default:
-        return new Aphront400Response();
-    }
+    $xaction =
+      HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
+        $action);
 
-    $build->assertCanIssueCommand($viewer, $action);
+    if (!$xaction) {
+      return new Aphront404Response();
+    }
 
     switch ($via) {
       case 'buildable':
@@ -50,100 +39,29 @@
         break;
     }
 
-    if ($request->isDialogFormPost() && $can_issue) {
-      $build->sendMessage($viewer, $action);
-      return id(new AphrontRedirectResponse())->setURI($return_uri);
+    try {
+      $xaction->assertCanSendMessage($viewer, $build);
+    } catch (HarbormasterMessageException $ex) {
+      return $this->newDialog()
+        ->setTitle($ex->getTitle())
+        ->appendChild($ex->getBody())
+        ->addCancelButton($return_uri);
     }
 
-    switch ($action) {
-      case HarbormasterBuildCommand::COMMAND_RESTART:
-        if ($can_issue) {
-          $title = pht('Really restart build?');
-          $body = pht(
-            'Progress on this build will be discarded and the build will '.
-            'restart. Side effects of the build will occur again. Really '.
-            'restart build?');
-          $submit = pht('Restart Build');
-        } else {
-          try {
-            $build->assertCanRestartBuild();
-            throw new Exception(pht('Expected to be unable to restart build.'));
-          } catch (HarbormasterRestartException $ex) {
-            $title = $ex->getTitle();
-            $body = $ex->getBody();
-          }
-        }
-        break;
-      case HarbormasterBuildCommand::COMMAND_ABORT:
-        if ($can_issue) {
-          $title = pht('Really abort build?');
-          $body = pht(
-            'Progress on this build will be discarded. Really '.
-            'abort build?');
-          $submit = pht('Abort Build');
-        } else {
-          $title = pht('Unable to Abort Build');
-          $body = pht('You can not abort this build.');
-        }
-        break;
-      case HarbormasterBuildCommand::COMMAND_PAUSE:
-        if ($can_issue) {
-          $title = pht('Really pause build?');
-          $body = pht(
-            'If you pause this build, work will halt once the current steps '.
-            'complete. You can resume the build later.');
-          $submit = pht('Pause Build');
-        } else {
-          $title = pht('Unable to Pause Build');
-          if ($build->isComplete()) {
-            $body = pht(
-              'This build is already complete. You can not pause a completed '.
-              'build.');
-          } else if ($build->isPaused()) {
-            $body = pht(
-              'This build is already paused. You can not pause a build which '.
-              'has already been paused.');
-          } else if ($build->isPausing()) {
-            $body = pht(
-              'This build is already pausing. You can not reissue a pause '.
-              'command to a pausing build.');
-          } else {
-            $body = pht(
-              'This build can not be paused.');
-          }
-        }
-        break;
-      case HarbormasterBuildCommand::COMMAND_RESUME:
-        if ($can_issue) {
-          $title = pht('Really resume build?');
-          $body = pht(
-            'Work will continue on the build. Really resume?');
-          $submit = pht('Resume Build');
-        } else {
-          $title = pht('Unable to Resume Build');
-          if ($build->isResuming()) {
-            $body = pht(
-              'This build is already resuming. You can not reissue a resume '.
-              'command to a resuming build.');
-          } else if (!$build->isPaused()) {
-            $body = pht(
-              'This build is not paused. You can only resume a paused '.
-              'build.');
-          }
-        }
-        break;
+    if ($request->isDialogFormPost()) {
+      $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType());
+      return id(new AphrontRedirectResponse())->setURI($return_uri);
     }
 
-    $dialog = $this->newDialog()
+    $title = $xaction->newConfirmPromptTitle();
+    $body = $xaction->newConfirmPromptBody();
+    $submit = $xaction->getHarbormasterBuildMessageName();
+
+    return $this->newDialog()
       ->setTitle($title)
       ->appendChild($body)
-      ->addCancelButton($return_uri);
-
-    if ($can_issue) {
-      $dialog->addSubmitButton($submit);
-    }
-
-    return $dialog;
+      ->addCancelButton($return_uri)
+      ->addSubmitButton($submit);
   }
 
 }
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
--- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
@@ -32,21 +32,13 @@
       ->setPolicyObject($build)
       ->setHeaderIcon('fa-cubes');
 
-    $is_restarting = $build->isRestarting();
-
-    if ($is_restarting) {
-      $page_header->setStatus(
-        'fa-exclamation-triangle', 'red', pht('Restarting'));
-    } else if ($build->isPausing()) {
-      $page_header->setStatus(
-        'fa-exclamation-triangle', 'red', pht('Pausing'));
-    } else if ($build->isResuming()) {
-      $page_header->setStatus(
-        'fa-exclamation-triangle', 'red', pht('Resuming'));
-    } else if ($build->isAborting()) {
-      $page_header->setStatus(
-        'fa-exclamation-triangle', 'red', pht('Aborting'));
-    }
+    $status = $build->getBuildPendingStatusObject();
+
+    $status_icon = $status->getIconIcon();
+    $status_color = $status->getIconColor();
+    $status_name = $status->getName();
+
+    $page_header->setStatus($status_icon, $status_color, $status_name);
 
     $max_generation = (int)$build->getBuildGeneration();
     if ($max_generation === 0) {
@@ -55,7 +47,7 @@
       $min_generation = 1;
     }
 
-    if ($is_restarting) {
+    if ($build->isRestarting()) {
       $max_generation = $max_generation + 1;
     }
 
@@ -541,63 +533,31 @@
 
     $curtain = $this->newCurtainView($build);
 
-    $can_restart =
-      $build->canRestartBuild() &&
-      $build->canIssueCommand(
-        $viewer,
-        HarbormasterBuildCommand::COMMAND_RESTART);
-
-    $can_pause =
-      $build->canPauseBuild() &&
-      $build->canIssueCommand(
-        $viewer,
-        HarbormasterBuildCommand::COMMAND_PAUSE);
-
-    $can_resume =
-      $build->canResumeBuild() &&
-      $build->canIssueCommand(
-        $viewer,
-        HarbormasterBuildCommand::COMMAND_RESUME);
-
-    $can_abort =
-      $build->canAbortBuild() &&
-      $build->canIssueCommand(
-        $viewer,
-        HarbormasterBuildCommand::COMMAND_ABORT);
-
-    $curtain->addAction(
-      id(new PhabricatorActionView())
-        ->setName(pht('Restart Build'))
-        ->setIcon('fa-repeat')
-        ->setHref($this->getApplicationURI('/build/restart/'.$id.'/'))
-        ->setDisabled(!$can_restart)
-        ->setWorkflow(true));
-
-    if ($build->canResumeBuild()) {
-      $curtain->addAction(
-        id(new PhabricatorActionView())
-          ->setName(pht('Resume Build'))
-          ->setIcon('fa-play')
-          ->setHref($this->getApplicationURI('/build/resume/'.$id.'/'))
-          ->setDisabled(!$can_resume)
-          ->setWorkflow(true));
-    } else {
-      $curtain->addAction(
-        id(new PhabricatorActionView())
-          ->setName(pht('Pause Build'))
-          ->setIcon('fa-pause')
-          ->setHref($this->getApplicationURI('/build/pause/'.$id.'/'))
-          ->setDisabled(!$can_pause)
-          ->setWorkflow(true));
-    }
+    $messages = array(
+      new HarbormasterBuildMessageRestartTransaction(),
+      new HarbormasterBuildMessagePauseTransaction(),
+      new HarbormasterBuildMessageResumeTransaction(),
+      new HarbormasterBuildMessageAbortTransaction(),
+    );
 
-    $curtain->addAction(
-      id(new PhabricatorActionView())
-        ->setName(pht('Abort Build'))
-        ->setIcon('fa-exclamation-triangle')
-        ->setHref($this->getApplicationURI('/build/abort/'.$id.'/'))
-        ->setDisabled(!$can_abort)
-        ->setWorkflow(true));
+    foreach ($messages as $message) {
+      $can_send = $message->canSendMessage($viewer, $build);
+
+      $message_uri = urisprintf(
+        '/build/%s/%d/',
+        $message->getHarbormasterBuildMessageType(),
+        $id);
+      $message_uri = $this->getApplicationURI($message_uri);
+
+      $action = id(new PhabricatorActionView())
+        ->setName($message->getHarbormasterBuildMessageName())
+        ->setIcon($message->getIcon())
+        ->setHref($message_uri)
+        ->setDisabled(!$can_send)
+        ->setWorkflow(true);
+
+      $curtain->addAction($action);
+    }
 
     return $curtain;
   }
@@ -624,10 +584,6 @@
       pht('Build Plan'),
       $handles[$build->getBuildPlanPHID()]->renderLink());
 
-    $properties->addProperty(
-      pht('Status'),
-      $this->getStatus($build));
-
     return id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Properties'))
       ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
@@ -681,31 +637,6 @@
       ->setTable($table);
   }
 
-
-  private function getStatus(HarbormasterBuild $build) {
-    $status_view = new PHUIStatusListView();
-
-    $item = new PHUIStatusItemView();
-
-    if ($build->isPausing()) {
-      $status_name = pht('Pausing');
-      $icon = PHUIStatusItemView::ICON_RIGHT;
-      $color = 'dark';
-    } else {
-      $status = $build->getBuildStatus();
-      $status_name =
-        HarbormasterBuildStatus::getBuildStatusName($status);
-      $icon = HarbormasterBuildStatus::getBuildStatusIcon($status);
-      $color = HarbormasterBuildStatus::getBuildStatusColor($status);
-    }
-
-    $item->setTarget($status_name);
-    $item->setIcon($icon, $color);
-    $status_view->addItem($item);
-
-    return $status_view;
-  }
-
   private function buildMessages(array $messages) {
     $viewer = $this->getRequest()->getUser();
 
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php
--- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php
@@ -22,299 +22,162 @@
       return new Aphront404Response();
     }
 
-    $issuable = array();
+    $message =
+      HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
+        $action);
+    if (!$message) {
+      return new Aphront404Response();
+    }
+
+    $return_uri = '/'.$buildable->getMonogram();
 
+    // See T13348. Actions may apply to only a subset of builds, so give the
+    // user a preview of what will happen.
+
+    $can_send = array();
+
+    $rows = array();
     $builds = $buildable->getBuilds();
     foreach ($builds as $key => $build) {
-      switch ($action) {
-        case HarbormasterBuildCommand::COMMAND_RESTART:
-          if ($build->canRestartBuild()) {
-            $issuable[$key] = $build;
-          }
-          break;
-        case HarbormasterBuildCommand::COMMAND_PAUSE:
-          if ($build->canPauseBuild()) {
-            $issuable[$key] = $build;
-          }
-          break;
-        case HarbormasterBuildCommand::COMMAND_RESUME:
-          if ($build->canResumeBuild()) {
-            $issuable[$key] = $build;
-          }
-          break;
-        case HarbormasterBuildCommand::COMMAND_ABORT:
-          if ($build->canAbortBuild()) {
-            $issuable[$key] = $build;
-          }
-          break;
-        default:
-          return new Aphront400Response();
+      $exception = null;
+      try {
+        $message->assertCanSendMessage($viewer, $build);
+        $can_send[$key] = $build;
+      } catch (HarbormasterMessageException $ex) {
+        $exception = $ex;
       }
-    }
 
-    $restricted = false;
-    foreach ($issuable as $key => $build) {
-      if (!$build->canIssueCommand($viewer, $action)) {
-        $restricted = true;
-        unset($issuable[$key]);
-      }
-    }
+      if (!$exception) {
+        $icon_icon = $message->getIcon();
+        $icon_color = 'green';
+
+        $title = $message->getHarbormasterBuildMessageName();
+        $body = $message->getHarbormasterBuildableMessageEffect();
+      } else {
+        $icon_icon = 'fa-times';
+        $icon_color = 'red';
 
-    $building = false;
-    foreach ($issuable as $key => $build) {
-      if ($build->isBuilding()) {
-        $building = true;
-        break;
+        $title = $ex->getTitle();
+        $body = $ex->getBody();
       }
+
+      $icon = id(new PHUIIconView())
+        ->setIcon($icon_icon)
+        ->setColor($icon_color);
+
+      $build_name = phutil_tag(
+        'a',
+        array(
+          'href' => $build->getURI(),
+          'target' => '_blank',
+        ),
+        pht('%s %s', $build->getObjectName(), $build->getName()));
+
+      $rows[] = array(
+        $icon,
+        $build_name,
+        $title,
+        $body,
+      );
     }
 
-    $return_uri = '/'.$buildable->getMonogram();
-    if ($request->isDialogFormPost() && $issuable) {
+    $table = id(new AphrontTableView($rows))
+      ->setHeaders(
+        array(
+          null,
+          pht('Build'),
+          pht('Action'),
+          pht('Details'),
+        ))
+      ->setColumnClasses(
+        array(
+          null,
+          null,
+          'pri',
+          'wide',
+        ));
+
+    $table = phutil_tag(
+      'div',
+      array(
+        'class' => 'mlt mlb',
+      ),
+      $table);
+
+    if ($request->isDialogFormPost() && $can_send) {
       $editor = id(new HarbormasterBuildableTransactionEditor())
         ->setActor($viewer)
         ->setContentSourceFromRequest($request)
         ->setContinueOnNoEffect(true)
         ->setContinueOnMissingFields(true);
 
+      $xaction_type = HarbormasterBuildableMessageTransaction::TRANSACTIONTYPE;
+
       $xaction = id(new HarbormasterBuildableTransaction())
-        ->setTransactionType(HarbormasterBuildableTransaction::TYPE_COMMAND)
+        ->setTransactionType($xaction_type)
         ->setNewValue($action);
 
       $editor->applyTransactions($buildable, array($xaction));
 
-      $build_editor = id(new HarbormasterBuildTransactionEditor())
-        ->setActor($viewer)
-        ->setContentSourceFromRequest($request)
-        ->setContinueOnNoEffect(true)
-        ->setContinueOnMissingFields(true);
-
-      foreach ($issuable as $build) {
-        $xaction = id(new HarbormasterBuildTransaction())
-          ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
-          ->setNewValue($action);
-        $build_editor->applyTransactions($build, array($xaction));
+      foreach ($can_send as $build) {
+        $build->sendMessage(
+          $viewer,
+          $message->getHarbormasterBuildMessageType());
       }
 
       return id(new AphrontRedirectResponse())->setURI($return_uri);
     }
 
-    $width = AphrontDialogView::WIDTH_DEFAULT;
-
-    switch ($action) {
-      case HarbormasterBuildCommand::COMMAND_RESTART:
-        // See T13348. The "Restart Builds" action may restart only a subset
-        // of builds, so show the user a preview of which builds will actually
-        // restart.
-
-        $body = array();
-
-        if ($issuable) {
-          $title = pht('Restart Builds');
-          $submit = pht('Restart Builds');
-        } else {
-          $title = pht('Unable to Restart Builds');
-        }
-
-        if ($builds) {
-          $width = AphrontDialogView::WIDTH_FORM;
-
-          $body[] = pht('Builds for this buildable:');
-
-          $rows = array();
-          foreach ($builds as $key => $build) {
-            if (isset($issuable[$key])) {
-              $icon = id(new PHUIIconView())
-                ->setIcon('fa-repeat green');
-              $build_note = pht('Will Restart');
-            } else {
-              $icon = null;
-
-              try {
-                $build->assertCanRestartBuild();
-              } catch (HarbormasterRestartException $ex) {
-                $icon = id(new PHUIIconView())
-                  ->setIcon('fa-times red');
-                $build_note = pht(
-                  '%s: %s',
-                  phutil_tag('strong', array(), pht('Not Restartable')),
-                  $ex->getTitle());
-              }
-
-              if (!$icon) {
-                try {
-                  $build->assertCanIssueCommand($viewer, $action);
-                } catch (PhabricatorPolicyException $ex) {
-                  $icon = id(new PHUIIconView())
-                    ->setIcon('fa-lock red');
-                  $build_note = pht(
-                    '%s: %s',
-                    phutil_tag('strong', array(), pht('Not Restartable')),
-                    pht('You do not have permission to restart this build.'));
-                }
-              }
-
-              if (!$icon) {
-                $icon = id(new PHUIIconView())
-                  ->setIcon('fa-times red');
-                $build_note = pht('Will Not Restart');
-              }
-            }
-
-            $build_name = phutil_tag(
-              'a',
-              array(
-                'href' => $build->getURI(),
-                'target' => '_blank',
-              ),
-              pht('%s %s', $build->getObjectName(), $build->getName()));
-
-            $rows[] = array(
-              $icon,
-              $build_name,
-              $build_note,
-            );
-          }
-
-          $table = id(new AphrontTableView($rows))
-            ->setHeaders(
-              array(
-                null,
-                pht('Build'),
-                pht('Action'),
-              ))
-            ->setColumnClasses(
-              array(
-                null,
-                'pri',
-                'wide',
-              ));
-
-          $table = phutil_tag(
-            'div',
-            array(
-              'class' => 'mlt mlb',
-            ),
-            $table);
-
-          $body[] = $table;
-        }
-
-        if ($issuable) {
-          $warnings = array();
-
-          if ($restricted) {
-            $warnings[] = pht(
-              'You only have permission to restart some builds.');
-          }
-
-          if ($building) {
-            $warnings[] = pht(
-              'Progress on running builds will be discarded.');
-          }
-
-          $warnings[] = pht(
-            'When a build is restarted, side effects associated with '.
-            'the build may occur again.');
-
-          $body[] = id(new PHUIInfoView())
-            ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
-            ->setErrors($warnings);
-
-          $body[] = pht('Really restart builds?');
-        } else {
-          if ($restricted) {
-            $body[] = pht('You do not have permission to restart any builds.');
-          } else {
-            $body[] = pht('No builds can be restarted.');
-          }
-        }
-
-        break;
-      case HarbormasterBuildCommand::COMMAND_PAUSE:
-        if ($issuable) {
-          $title = pht('Really pause builds?');
-
-          if ($restricted) {
-            $body = pht(
-              'You only have permission to pause some builds. Once the '.
-              'current steps complete, work will halt on builds you have '.
-              'permission to pause. You can resume the builds later.');
-          } else {
-            $body = pht(
-              'If you pause all builds, work will halt once the current steps '.
-              'complete. You can resume the builds later.');
-          }
-          $submit = pht('Pause Builds');
-        } else {
-          $title = pht('Unable to Pause Builds');
-
-          if ($restricted) {
-            $body = pht('You do not have permission to pause any builds.');
-          } else {
-            $body = pht('No builds can be paused.');
-          }
-        }
-        break;
-      case HarbormasterBuildCommand::COMMAND_ABORT:
-        if ($issuable) {
-          $title = pht('Really abort builds?');
-          if ($restricted) {
-            $body = pht(
-              'You only have permission to abort some builds. Work will '.
-              'halt immediately on builds you have permission to abort. '.
-              'Progress will be discarded, and builds must be completely '.
-              'restarted if you want them to complete.');
-          } else {
-            $body = pht(
-              'If you abort all builds, work will halt immediately. Work '.
-              'will be discarded, and builds must be completely restarted.');
-          }
-          $submit = pht('Abort Builds');
-        } else {
-          $title = pht('Unable to Abort Builds');
-
-          if ($restricted) {
-            $body = pht('You do not have permission to abort any builds.');
-          } else {
-            $body = pht('No builds can be aborted.');
-          }
-        }
-        break;
-      case HarbormasterBuildCommand::COMMAND_RESUME:
-        if ($issuable) {
-          $title = pht('Really resume builds?');
-          if ($restricted) {
-            $body = pht(
-              'You only have permission to resume some builds. Work will '.
-              'continue on builds you have permission to resume.');
-          } else {
-            $body = pht('Work will continue on all builds. Really resume?');
-          }
-
-          $submit = pht('Resume Builds');
-        } else {
-          $title = pht('Unable to Resume Builds');
-          if ($restricted) {
-            $body = pht('You do not have permission to resume any builds.');
-          } else {
-            $body = pht('No builds can be resumed.');
-          }
-        }
-        break;
+    if (!$builds) {
+      $title = pht('No Builds');
+      $body = pht(
+        'This buildable has no builds, so you can not issue any commands.');
+    } else {
+      if ($can_send) {
+        $title = $message->newBuildableConfirmPromptTitle(
+          $builds,
+          $can_send);
+
+        $body = $message->newBuildableConfirmPromptBody(
+          $builds,
+          $can_send);
+      } else {
+        $title = pht('Unable to Send Command');
+        $body = pht(
+          'You can not send this command to any of the current builds '.
+          'for this buildable.');
+      }
+
+      $body = array(
+        pht('Builds for this buildable:'),
+        $table,
+        $body,
+      );
     }
 
-    $dialog = id(new AphrontDialogView())
-      ->setUser($viewer)
-      ->setWidth($width)
+    $warnings = $message->newBuildableConfirmPromptWarnings(
+      $builds,
+      $can_send);
+
+    if ($warnings) {
+      $body[] = id(new PHUIInfoView())
+        ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+        ->setErrors($warnings);
+    }
+
+    $submit = $message->getHarbormasterBuildableMessageName();
+
+    $dialog = $this->newDialog()
+      ->setWidth(AphrontDialogView::WIDTH_FULL)
       ->setTitle($title)
       ->appendChild($body)
       ->addCancelButton($return_uri);
 
-    if ($issuable) {
+    if ($can_send) {
       $dialog->addSubmitButton($submit);
     }
 
-    return id(new AphrontDialogResponse())->setDialog($dialog);
+    return $dialog;
   }
 
 }
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
--- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
@@ -87,75 +87,39 @@
       $buildable,
       PhabricatorPolicyCapability::CAN_EDIT);
 
-    $can_restart = false;
-    $can_resume = false;
-    $can_pause = false;
-    $can_abort = false;
-
-    $command_restart = HarbormasterBuildCommand::COMMAND_RESTART;
-    $command_resume = HarbormasterBuildCommand::COMMAND_RESUME;
-    $command_pause = HarbormasterBuildCommand::COMMAND_PAUSE;
-    $command_abort = HarbormasterBuildCommand::COMMAND_ABORT;
-
-    foreach ($buildable->getBuilds() as $build) {
-      if ($build->canRestartBuild()) {
-        if ($build->canIssueCommand($viewer, $command_restart)) {
-          $can_restart = true;
-        }
-      }
-      if ($build->canResumeBuild()) {
-        if ($build->canIssueCommand($viewer, $command_resume)) {
-          $can_resume = true;
-        }
-      }
-      if ($build->canPauseBuild()) {
-        if ($build->canIssueCommand($viewer, $command_pause)) {
-          $can_pause = true;
-        }
-      }
-      if ($build->canAbortBuild()) {
-        if ($build->canIssueCommand($viewer, $command_abort)) {
-          $can_abort = true;
+    $messages = array(
+      new HarbormasterBuildMessageRestartTransaction(),
+      new HarbormasterBuildMessagePauseTransaction(),
+      new HarbormasterBuildMessageResumeTransaction(),
+      new HarbormasterBuildMessageAbortTransaction(),
+    );
+
+    foreach ($messages as $message) {
+
+      // Messages are enabled if they can be sent to at least one build.
+      $can_send = false;
+      foreach ($buildable->getBuilds() as $build) {
+        $can_send = $message->canSendMessage($viewer, $build);
+        if ($can_send) {
+          break;
         }
       }
-    }
 
-    $restart_uri = "buildable/{$id}/restart/";
-    $pause_uri = "buildable/{$id}/pause/";
-    $resume_uri = "buildable/{$id}/resume/";
-    $abort_uri = "buildable/{$id}/abort/";
-
-    $curtain->addAction(
-      id(new PhabricatorActionView())
-        ->setIcon('fa-repeat')
-        ->setName(pht('Restart Builds'))
-        ->setHref($this->getApplicationURI($restart_uri))
-        ->setWorkflow(true)
-        ->setDisabled(!$can_restart || !$can_edit));
-
-    $curtain->addAction(
-      id(new PhabricatorActionView())
-        ->setIcon('fa-pause')
-        ->setName(pht('Pause Builds'))
-        ->setHref($this->getApplicationURI($pause_uri))
-        ->setWorkflow(true)
-        ->setDisabled(!$can_pause || !$can_edit));
-
-    $curtain->addAction(
-      id(new PhabricatorActionView())
-        ->setIcon('fa-play')
-        ->setName(pht('Resume Builds'))
-        ->setHref($this->getApplicationURI($resume_uri))
-        ->setWorkflow(true)
-        ->setDisabled(!$can_resume || !$can_edit));
-
-    $curtain->addAction(
-      id(new PhabricatorActionView())
-        ->setIcon('fa-exclamation-triangle')
-        ->setName(pht('Abort Builds'))
-        ->setHref($this->getApplicationURI($abort_uri))
-        ->setWorkflow(true)
-        ->setDisabled(!$can_abort || !$can_edit));
+      $message_uri = urisprintf(
+        '/buildable/%d/%s/',
+        $id,
+        $message->getHarbormasterBuildMessageType());
+      $message_uri = $this->getApplicationURI($message_uri);
+
+      $action = id(new PhabricatorActionView())
+        ->setName($message->getHarbormasterBuildableMessageName())
+        ->setIcon($message->getIcon())
+        ->setHref($message_uri)
+        ->setDisabled(!$can_send || !$can_edit)
+        ->setWorkflow(true);
+
+      $curtain->addAction($action);
+    }
 
     return $curtain;
   }
@@ -198,56 +162,17 @@
       ->setUser($viewer);
     foreach ($buildable->getBuilds() as $build) {
       $view_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
+
       $item = id(new PHUIObjectItemView())
         ->setObjectName(pht('Build %d', $build->getID()))
         ->setHeader($build->getName())
         ->setHref($view_uri);
 
-      $status = $build->getBuildStatus();
-      $status_color = HarbormasterBuildStatus::getBuildStatusColor($status);
-      $status_name = HarbormasterBuildStatus::getBuildStatusName($status);
-      $item->setStatusIcon('fa-dot-circle-o '.$status_color, $status_name);
-      $item->addAttribute($status_name);
-
-      if ($build->isRestarting()) {
-        $item->addIcon('fa-repeat', pht('Restarting'));
-      } else if ($build->isPausing()) {
-        $item->addIcon('fa-pause', pht('Pausing'));
-      } else if ($build->isResuming()) {
-        $item->addIcon('fa-play', pht('Resuming'));
-      }
+      $status = $build->getBuildPendingStatusObject();
 
-      $build_id = $build->getID();
-
-      $restart_uri = "build/restart/{$build_id}/buildable/";
-      $resume_uri = "build/resume/{$build_id}/buildable/";
-      $pause_uri = "build/pause/{$build_id}/buildable/";
-      $abort_uri = "build/abort/{$build_id}/buildable/";
-
-      $item->addAction(
-        id(new PHUIListItemView())
-          ->setIcon('fa-repeat')
-          ->setName(pht('Restart'))
-          ->setHref($this->getApplicationURI($restart_uri))
-          ->setWorkflow(true)
-          ->setDisabled(!$build->canRestartBuild()));
-
-      if ($build->canResumeBuild()) {
-        $item->addAction(
-          id(new PHUIListItemView())
-            ->setIcon('fa-play')
-            ->setName(pht('Resume'))
-            ->setHref($this->getApplicationURI($resume_uri))
-            ->setWorkflow(true));
-      } else {
-        $item->addAction(
-          id(new PHUIListItemView())
-            ->setIcon('fa-pause')
-            ->setName(pht('Pause'))
-            ->setHref($this->getApplicationURI($pause_uri))
-            ->setWorkflow(true)
-            ->setDisabled(!$build->canPauseBuild()));
-      }
+      $item->setStatusIcon(
+        $status->getIconIcon().' '.$status->getIconColor(),
+        $status->getName());
 
       $targets = $build->getBuildTargets();
 
diff --git a/src/applications/harbormaster/integration/buildkite/HarbormasterBuildkiteHookHandler.php b/src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php
rename from src/applications/harbormaster/integration/buildkite/HarbormasterBuildkiteHookHandler.php
rename to src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php
--- a/src/applications/harbormaster/integration/buildkite/HarbormasterBuildkiteHookHandler.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php
@@ -1,10 +1,10 @@
 <?php
 
-final class HarbormasterBuildkiteHookHandler
-  extends HarbormasterHookHandler {
+final class HarbormasterBuildkiteHookController
+  extends HarbormasterController {
 
-  public function getName() {
-    return 'buildkite';
+  public function shouldRequireLogin() {
+    return false;
   }
 
   /**
diff --git a/src/applications/harbormaster/integration/circleci/HarbormasterCircleCIHookHandler.php b/src/applications/harbormaster/controller/HarbormasterCircleCIHookController.php
rename from src/applications/harbormaster/integration/circleci/HarbormasterCircleCIHookHandler.php
rename to src/applications/harbormaster/controller/HarbormasterCircleCIHookController.php
--- a/src/applications/harbormaster/integration/circleci/HarbormasterCircleCIHookHandler.php
+++ b/src/applications/harbormaster/controller/HarbormasterCircleCIHookController.php
@@ -1,10 +1,10 @@
 <?php
 
-final class HarbormasterCircleCIHookHandler
-  extends HarbormasterHookHandler {
+final class HarbormasterCircleCIHookController
+  extends HarbormasterController {
 
-  public function getName() {
-    return 'circleci';
+  public function shouldRequireLogin() {
+    return false;
   }
 
   /**
diff --git a/src/applications/harbormaster/controller/HarbormasterHookController.php b/src/applications/harbormaster/controller/HarbormasterHookController.php
deleted file mode 100644
--- a/src/applications/harbormaster/controller/HarbormasterHookController.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-final class HarbormasterHookController
-  extends HarbormasterController {
-
-  public function shouldRequireLogin() {
-    return false;
-  }
-
-  public function handleRequest(AphrontRequest $request) {
-    $name = $request->getURIData('handler');
-    $handler = HarbormasterHookHandler::getHandler($name);
-
-    if (!$handler) {
-      throw new Exception(pht('No handler found for %s', $name));
-    }
-
-    return $handler->handleRequest($request);
-  }
-
-}
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildEditEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/editor/HarbormasterBuildEditEngine.php
@@ -0,0 +1,87 @@
+<?php
+
+final class HarbormasterBuildEditEngine
+  extends PhabricatorEditEngine {
+
+  const ENGINECONST = 'harbormaster.build';
+
+  public function isEngineConfigurable() {
+    return false;
+  }
+
+  public function getEngineName() {
+    return pht('Harbormaster Builds');
+  }
+
+  public function getSummaryHeader() {
+    return pht('Edit Harbormaster Build Configurations');
+  }
+
+  public function getSummaryText() {
+    return pht('This engine is used to edit Harbormaster builds.');
+  }
+
+  public function getEngineApplicationClass() {
+    return 'PhabricatorHarbormasterApplication';
+  }
+
+  protected function newEditableObject() {
+    $viewer = $this->getViewer();
+    return HarbormasterBuild::initializeNewBuild($viewer);
+  }
+
+  protected function newObjectQuery() {
+    return new HarbormasterBuildQuery();
+  }
+
+  protected function newEditableObjectForDocumentation() {
+    $object = new DifferentialRevision();
+
+    $buildable = id(new HarbormasterBuildable())
+      ->attachBuildableObject($object);
+
+    return $this->newEditableObject()
+      ->attachBuildable($buildable);
+  }
+
+  protected function getObjectCreateTitleText($object) {
+    return pht('Create Build');
+  }
+
+  protected function getObjectCreateButtonText($object) {
+    return pht('Create Build');
+  }
+
+  protected function getObjectEditTitleText($object) {
+    return pht('Edit Build: %s', $object->getName());
+  }
+
+  protected function getObjectEditShortText($object) {
+    return pht('Edit Build');
+  }
+
+  protected function getObjectCreateShortText() {
+    return pht('Create Build');
+  }
+
+  protected function getObjectName() {
+    return pht('Build');
+  }
+
+  protected function getEditorURI() {
+    return '/harbormaster/build/edit/';
+  }
+
+  protected function getObjectCreateCancelURI($object) {
+    return '/harbormaster/';
+  }
+
+  protected function getObjectViewURI($object) {
+    return $object->getURI();
+  }
+
+  protected function buildCustomEditFields($object) {
+    return array();
+  }
+
+}
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php
--- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php
@@ -11,115 +11,4 @@
     return pht('Harbormaster Builds');
   }
 
-  public function getTransactionTypes() {
-    $types = parent::getTransactionTypes();
-
-    $types[] = HarbormasterBuildTransaction::TYPE_CREATE;
-    $types[] = HarbormasterBuildTransaction::TYPE_COMMAND;
-
-    return $types;
-  }
-
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildTransaction::TYPE_CREATE:
-      case HarbormasterBuildTransaction::TYPE_COMMAND:
-        return null;
-    }
-
-    return parent::getCustomTransactionOldValue($object, $xaction);
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildTransaction::TYPE_CREATE:
-        return true;
-      case HarbormasterBuildTransaction::TYPE_COMMAND:
-        return $xaction->getNewValue();
-    }
-
-    return parent::getCustomTransactionNewValue($object, $xaction);
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildTransaction::TYPE_CREATE:
-        return;
-      case HarbormasterBuildTransaction::TYPE_COMMAND:
-        return $this->executeBuildCommand($object, $xaction);
-    }
-
-    return parent::applyCustomInternalTransaction($object, $xaction);
-  }
-
-  private function executeBuildCommand(
-    HarbormasterBuild $build,
-    HarbormasterBuildTransaction $xaction) {
-
-    $command = $xaction->getNewValue();
-
-    switch ($command) {
-      case HarbormasterBuildCommand::COMMAND_RESTART:
-        $issuable = $build->canRestartBuild();
-        break;
-      case HarbormasterBuildCommand::COMMAND_PAUSE:
-        $issuable = $build->canPauseBuild();
-        break;
-      case HarbormasterBuildCommand::COMMAND_RESUME:
-        $issuable = $build->canResumeBuild();
-        break;
-      case HarbormasterBuildCommand::COMMAND_ABORT:
-        $issuable = $build->canAbortBuild();
-        break;
-      default:
-        throw new Exception(pht('Unknown command %s', $command));
-    }
-
-    if (!$issuable) {
-      return;
-    }
-
-    $actor = $this->getActor();
-    if (!$build->canIssueCommand($actor, $command)) {
-      return;
-    }
-
-    id(new HarbormasterBuildCommand())
-      ->setAuthorPHID($xaction->getAuthorPHID())
-      ->setTargetPHID($build->getPHID())
-      ->setCommand($command)
-      ->save();
-
-    PhabricatorWorker::scheduleTask(
-      'HarbormasterBuildWorker',
-      array(
-        'buildID' => $build->getID(),
-      ),
-      array(
-        'objectPHID' => $build->getPHID(),
-      ));
-  }
-
-  protected function applyCustomExternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildTransaction::TYPE_CREATE:
-      case HarbormasterBuildTransaction::TYPE_COMMAND:
-        return;
-    }
-
-    return parent::applyCustomExternalTransaction($object, $xaction);
-  }
-
 }
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildableEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildableEditEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/editor/HarbormasterBuildableEditEngine.php
@@ -0,0 +1,84 @@
+<?php
+
+final class HarbormasterBuildableEditEngine
+  extends PhabricatorEditEngine {
+
+  const ENGINECONST = 'harbormaster.buildable';
+
+  public function isEngineConfigurable() {
+    return false;
+  }
+
+  public function getEngineName() {
+    return pht('Harbormaster Buildables');
+  }
+
+  public function getSummaryHeader() {
+    return pht('Edit Harbormaster Buildable Configurations');
+  }
+
+  public function getSummaryText() {
+    return pht('This engine is used to edit Harbormaster buildables.');
+  }
+
+  public function getEngineApplicationClass() {
+    return 'PhabricatorHarbormasterApplication';
+  }
+
+  protected function newEditableObject() {
+    $viewer = $this->getViewer();
+    return HarbormasterBuildable::initializeNewBuildable($viewer);
+  }
+
+  protected function newObjectQuery() {
+    return new HarbormasterBuildableQuery();
+  }
+
+  protected function newEditableObjectForDocumentation() {
+    $object = new DifferentialRevision();
+
+    return $this->newEditableObject()
+      ->attachBuildableObject($object);
+  }
+
+  protected function getObjectCreateTitleText($object) {
+    return pht('Create Buildable');
+  }
+
+  protected function getObjectCreateButtonText($object) {
+    return pht('Create Buildable');
+  }
+
+  protected function getObjectEditTitleText($object) {
+    return pht('Edit Buildable: %s', $object->getName());
+  }
+
+  protected function getObjectEditShortText($object) {
+    return pht('Edit Buildable');
+  }
+
+  protected function getObjectCreateShortText() {
+    return pht('Create Buildable');
+  }
+
+  protected function getObjectName() {
+    return pht('Buildable');
+  }
+
+  protected function getEditorURI() {
+    return '/harbormaster/buildable/edit/';
+  }
+
+  protected function getObjectCreateCancelURI($object) {
+    return '/harbormaster/';
+  }
+
+  protected function getObjectViewURI($object) {
+    return $object->getURI();
+  }
+
+  protected function buildCustomEditFields($object) {
+    return array();
+  }
+
+}
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php
--- a/src/applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php
@@ -11,66 +11,4 @@
     return pht('Harbormaster Buildables');
   }
 
-  public function getTransactionTypes() {
-    $types = parent::getTransactionTypes();
-
-    $types[] = HarbormasterBuildableTransaction::TYPE_CREATE;
-    $types[] = HarbormasterBuildableTransaction::TYPE_COMMAND;
-
-    return $types;
-  }
-
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildableTransaction::TYPE_CREATE:
-      case HarbormasterBuildableTransaction::TYPE_COMMAND:
-        return null;
-    }
-
-    return parent::getCustomTransactionOldValue($object, $xaction);
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildableTransaction::TYPE_CREATE:
-        return true;
-      case HarbormasterBuildableTransaction::TYPE_COMMAND:
-        return $xaction->getNewValue();
-    }
-
-    return parent::getCustomTransactionNewValue($object, $xaction);
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildableTransaction::TYPE_CREATE:
-      case HarbormasterBuildableTransaction::TYPE_COMMAND:
-        return;
-    }
-
-    return parent::applyCustomInternalTransaction($object, $xaction);
-  }
-
-  protected function applyCustomExternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case HarbormasterBuildableTransaction::TYPE_CREATE:
-      case HarbormasterBuildableTransaction::TYPE_COMMAND:
-        return;
-    }
-
-    return parent::applyCustomExternalTransaction($object, $xaction);
-  }
-
 }
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
--- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
@@ -49,6 +49,7 @@
   }
 
   public function continueBuild() {
+    $viewer = $this->getViewer();
     $build = $this->getBuild();
 
     $lock_key = 'harbormaster.build:'.$build->getID();
@@ -68,7 +69,7 @@
 
       $lock->unlock();
 
-      $this->releaseAllArtifacts($build);
+      $build->releaseAllArtifacts($viewer);
 
       throw $ex;
     }
@@ -99,56 +100,66 @@
 
     // If we are no longer building for any reason, release all artifacts.
     if (!$build->isBuilding()) {
-      $this->releaseAllArtifacts($build);
+      $build->releaseAllArtifacts($viewer);
     }
   }
 
   private function updateBuild(HarbormasterBuild $build) {
-    if ($build->isAborting()) {
-      $this->releaseAllArtifacts($build);
-      $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED);
-      $build->save();
-    }
+    $viewer = $this->getViewer();
 
-    if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) ||
-        ($build->isRestarting())) {
-      $this->restartBuild($build);
-      $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
-      $build->save();
-    }
+    $content_source = PhabricatorContentSource::newForSource(
+      PhabricatorDaemonContentSource::SOURCECONST);
 
-    if ($build->isResuming()) {
-      $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
-      $build->save();
+    $acting_phid = $viewer->getPHID();
+    if (!$acting_phid) {
+      $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
     }
 
-    if ($build->isPausing() && !$build->isComplete()) {
-      $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED);
-      $build->save();
-    }
+    $editor = $build->getApplicationTransactionEditor()
+      ->setActor($viewer)
+      ->setActingAsPHID($acting_phid)
+      ->setContentSource($content_source)
+      ->setContinueOnNoEffect(true)
+      ->setContinueOnMissingFields(true);
 
-    $build->deleteUnprocessedCommands();
+    $xactions = array();
 
-    if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) {
-      $this->updateBuildSteps($build);
-    }
-  }
+    $messages = $build->getUnprocessedMessagesForApply();
+    foreach ($messages as $message) {
+      $message_type = $message->getType();
 
-  private function restartBuild(HarbormasterBuild $build) {
+      $message_xaction =
+        HarbormasterBuildMessageTransaction::getTransactionTypeForMessageType(
+          $message_type);
 
-    // We're restarting the build, so release all previous artifacts.
-    $this->releaseAllArtifacts($build);
+      if (!$message_xaction) {
+        continue;
+      }
 
-    // Increment the build generation counter on the build.
-    $build->setBuildGeneration($build->getBuildGeneration() + 1);
+      $xactions[] = $build->getApplicationTransactionTemplate()
+        ->setAuthorPHID($message->getAuthorPHID())
+        ->setTransactionType($message_xaction)
+        ->setNewValue($message_type);
+    }
+
+    if (!$xactions) {
+      if ($build->isPending()) {
+        // TODO: This should be a transaction.
+
+        $build->restartBuild($viewer);
+        $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
+        $build->save();
+      }
+    }
 
-    // Currently running targets should periodically check their build
-    // generation (which won't have changed) against the build's generation.
-    // If it is different, they will automatically stop what they're doing
-    // and abort.
+    if ($xactions) {
+      $editor->applyTransactions($build, $xactions);
+      $build->markUnprocessedMessagesAsProcessed();
+    }
 
-    // Previously we used to delete targets, logs and artifacts here. Instead,
-    // leave them around so users can view previous generations of this build.
+    if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) {
+      $this->updateBuildSteps($build);
+    }
   }
 
   private function updateBuildSteps(HarbormasterBuild $build) {
@@ -596,29 +607,6 @@
       ->publishBuildable($old, $new);
   }
 
-  private function releaseAllArtifacts(HarbormasterBuild $build) {
-    $targets = id(new HarbormasterBuildTargetQuery())
-      ->setViewer(PhabricatorUser::getOmnipotentUser())
-      ->withBuildPHIDs(array($build->getPHID()))
-      ->withBuildGenerations(array($build->getBuildGeneration()))
-      ->execute();
-
-    if (count($targets) === 0) {
-      return;
-    }
-
-    $target_phids = mpull($targets, 'getPHID');
-
-    $artifacts = id(new HarbormasterBuildArtifactQuery())
-      ->setViewer(PhabricatorUser::getOmnipotentUser())
-      ->withBuildTargetPHIDs($target_phids)
-      ->withIsReleased(false)
-      ->execute();
-    foreach ($artifacts as $artifact) {
-      $artifact->releaseArtifact();
-    }
-  }
-
   private function releaseQueuedArtifacts() {
     foreach ($this->artifactReleaseQueue as $key => $artifact) {
       $artifact->releaseArtifact();
diff --git a/src/applications/harbormaster/exception/HarbormasterRestartException.php b/src/applications/harbormaster/exception/HarbormasterMessageException.php
rename from src/applications/harbormaster/exception/HarbormasterRestartException.php
rename to src/applications/harbormaster/exception/HarbormasterMessageException.php
--- a/src/applications/harbormaster/exception/HarbormasterRestartException.php
+++ b/src/applications/harbormaster/exception/HarbormasterMessageException.php
@@ -1,6 +1,6 @@
 <?php
 
-final class HarbormasterRestartException extends Exception {
+final class HarbormasterMessageException extends Exception {
 
   private $title;
   private $body = array();
@@ -9,7 +9,11 @@
     $this->setTitle($title);
     $this->appendParagraph($body);
 
-    parent::__construct($title);
+    parent::__construct(
+      pht(
+        '%s: %s',
+        $title,
+        $body));
   }
 
   public function setTitle($title) {
@@ -30,4 +34,13 @@
     return $this->body;
   }
 
+  public function newDisplayString() {
+    $title = $this->getTitle();
+
+    $body = $this->getBody();
+    $body = implode("\n\n", $body);
+
+    return pht('%s: %s', $title, $body);
+  }
+
 }
diff --git a/src/applications/harbormaster/integration/HarbormasterHookHandler.php b/src/applications/harbormaster/integration/HarbormasterHookHandler.php
deleted file mode 100644
--- a/src/applications/harbormaster/integration/HarbormasterHookHandler.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-abstract class HarbormasterHookHandler
-  extends Phobject {
-
-  public static function getHandlers() {
-    return id(new PhutilClassMapQuery())
-      ->setAncestorClass(__CLASS__)
-      ->setUniqueMethod('getName')
-      ->execute();
-  }
-
-  public static function getHandler($handler) {
-    $base = idx(self::getHandlers(), $handler);
-
-    if ($base) {
-      return (clone $base);
-    }
-
-    return null;
-  }
-
-  abstract public function getName();
-
-  abstract public function handleRequest(AphrontRequest $request);
-
-}
diff --git a/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php
--- a/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php
+++ b/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php
@@ -32,10 +32,10 @@
 
     if (!$ids && !$active) {
       throw new PhutilArgumentUsageException(
-        pht('Use --id or --active to select builds.'));
+        pht('Use "--id" or "--active" to select builds.'));
     } if ($ids && $active) {
       throw new PhutilArgumentUsageException(
-        pht('Use one of --id or --active to select builds, but not both.'));
+        pht('Use one of "--id" or "--active" to select builds, but not both.'));
     }
 
     $query = id(new HarbormasterBuildQuery())
@@ -48,50 +48,41 @@
     }
     $builds = $query->execute();
 
-    $console = PhutilConsole::getConsole();
     $count = count($builds);
     if (!$count) {
-      $console->writeOut("%s\n", pht('No builds to restart.'));
+      $this->logSkip(
+        pht('SKIP'),
+        pht('No builds to restart.'));
       return 0;
     }
+
     $prompt = pht('Restart %s build(s)?', new PhutilNumber($count));
     if (!phutil_console_confirm($prompt)) {
-      $console->writeOut("%s\n", pht('Cancelled.'));
-      return 1;
+      throw new ArcanistUserAbortException();
     }
 
-    $app_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
-    $editor = id(new HarbormasterBuildTransactionEditor())
-      ->setActor($viewer)
-      ->setActingAsPHID($app_phid)
-      ->setContentSource($this->newContentSource());
+    $message = new HarbormasterBuildMessageRestartTransaction();
+
     foreach ($builds as $build) {
-      $console->writeOut(
-        "<bg:blue> %s </bg> %s\n",
+      $this->logInfo(
         pht('RESTARTING'),
         pht('Build %d: %s', $build->getID(), $build->getName()));
-      if (!$build->canRestartBuild()) {
-        $console->writeOut(
-          "<bg:yellow> %s </bg> %s\n",
-          pht('INVALID'),
-          pht('Cannot be restarted.'));
-        continue;
-      }
-      $xactions = array();
-      $xactions[] = id(new HarbormasterBuildTransaction())
-        ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
-        ->setNewValue(HarbormasterBuildCommand::COMMAND_RESTART);
+
       try {
-        $editor->applyTransactions($build, $xactions);
-      } catch (Exception $e) {
-        $message = phutil_console_wrap($e->getMessage(), 2);
-        $console->writeOut(
-          "<bg:red> %s </bg>\n%s\n",
-          pht('FAILED'),
-          $message);
-        continue;
+        $message->assertCanSendMessage($viewer, $build);
+      } catch (HarbormasterMessageException $ex) {
+        $this->logWarn(
+          pht('INVALID'),
+          $ex->newDisplayString());
       }
-      $console->writeOut("<bg:green> %s </bg>\n", pht('SUCCESS'));
+
+      $build->sendMessage(
+        $viewer,
+        $message->getHarbormasterBuildMessageType());
+
+      $this->logOkay(
+        pht('QUEUED'),
+        pht('Sent a restart message to build.'));
     }
 
     return 0;
diff --git a/src/applications/harbormaster/query/HarbormasterBuildQuery.php b/src/applications/harbormaster/query/HarbormasterBuildQuery.php
--- a/src/applications/harbormaster/query/HarbormasterBuildQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildQuery.php
@@ -104,13 +104,13 @@
     }
 
     $build_phids = mpull($page, 'getPHID');
-    $commands = id(new HarbormasterBuildCommand())->loadAllWhere(
-      'targetPHID IN (%Ls) ORDER BY id ASC',
+    $messages = id(new HarbormasterBuildMessage())->loadAllWhere(
+      'receiverPHID IN (%Ls) AND isConsumed = 0 ORDER BY id ASC',
       $build_phids);
-    $commands = mgroup($commands, 'getTargetPHID');
+    $messages = mgroup($messages, 'getReceiverPHID');
     foreach ($page as $build) {
-      $unprocessed_commands = idx($commands, $build->getPHID(), array());
-      $build->attachUnprocessedCommands($unprocessed_commands);
+      $unprocessed_messages = idx($messages, $build->getPHID(), array());
+      $build->attachUnprocessedMessages($unprocessed_messages);
     }
 
     if ($this->needBuildTargets) {
diff --git a/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php
@@ -122,7 +122,7 @@
     foreach ($abort_builds as $abort_build) {
       $abort_build->sendMessage(
         $viewer,
-        HarbormasterBuildCommand::COMMAND_ABORT);
+        HarbormasterBuildMessageAbortTransaction::MESSAGETYPE);
     }
   }
 
diff --git a/src/applications/harbormaster/integration/buildkite/HarbormasterBuildkiteBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
rename from src/applications/harbormaster/integration/buildkite/HarbormasterBuildkiteBuildStepImplementation.php
rename to src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
diff --git a/src/applications/harbormaster/integration/circleci/HarbormasterCircleCIBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
rename from src/applications/harbormaster/integration/circleci/HarbormasterCircleCIBuildStepImplementation.php
rename to src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildCommand.php b/src/applications/harbormaster/storage/HarbormasterBuildCommand.php
deleted file mode 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildCommand.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-final class HarbormasterBuildCommand extends HarbormasterDAO {
-
-  const COMMAND_PAUSE = 'pause';
-  const COMMAND_RESUME = 'resume';
-  const COMMAND_RESTART = 'restart';
-  const COMMAND_ABORT = 'abort';
-
-  protected $authorPHID;
-  protected $targetPHID;
-  protected $command;
-
-  protected function getConfiguration() {
-    return array(
-      self::CONFIG_COLUMN_SCHEMA => array(
-        'command' => 'text128',
-      ),
-      self::CONFIG_KEY_SCHEMA => array(
-        'key_target' => array(
-          'columns' => array('targetPHID'),
-        ),
-      ),
-    ) + parent::getConfiguration();
-  }
-
-}
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php b/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php
--- a/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php
@@ -1,10 +1,7 @@
 <?php
 
 final class HarbormasterBuildTransaction
-  extends PhabricatorApplicationTransaction {
-
-  const TYPE_CREATE = 'harbormaster:build:create';
-  const TYPE_COMMAND = 'harbormaster:build:command';
+  extends PhabricatorModularTransaction {
 
   public function getApplicationName() {
     return 'harbormaster';
@@ -14,81 +11,8 @@
     return HarbormasterBuildPHIDType::TYPECONST;
   }
 
-  public function getTitle() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CREATE:
-        return pht(
-          '%s created this build.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_COMMAND:
-        switch ($new) {
-          case HarbormasterBuildCommand::COMMAND_RESTART:
-            return pht(
-              '%s restarted this build.',
-              $this->renderHandleLink($author_phid));
-          case HarbormasterBuildCommand::COMMAND_ABORT:
-            return pht(
-              '%s aborted this build.',
-              $this->renderHandleLink($author_phid));
-          case HarbormasterBuildCommand::COMMAND_RESUME:
-            return pht(
-              '%s resumed this build.',
-              $this->renderHandleLink($author_phid));
-          case HarbormasterBuildCommand::COMMAND_PAUSE:
-            return pht(
-              '%s paused this build.',
-              $this->renderHandleLink($author_phid));
-        }
-    }
-    return parent::getTitle();
+  public function getBaseTransactionClass() {
+    return 'HarbormasterBuildTransactionType';
   }
 
-  public function getIcon() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CREATE:
-        return 'fa-plus';
-      case self::TYPE_COMMAND:
-        switch ($new) {
-          case HarbormasterBuildCommand::COMMAND_RESTART:
-            return 'fa-backward';
-          case HarbormasterBuildCommand::COMMAND_RESUME:
-            return 'fa-play';
-          case HarbormasterBuildCommand::COMMAND_PAUSE:
-            return 'fa-pause';
-          case HarbormasterBuildCommand::COMMAND_ABORT:
-            return 'fa-exclamation-triangle';
-        }
-    }
-
-    return parent::getIcon();
-  }
-
-  public function getColor() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CREATE:
-        return 'green';
-      case self::TYPE_COMMAND:
-        switch ($new) {
-          case HarbormasterBuildCommand::COMMAND_PAUSE:
-          case HarbormasterBuildCommand::COMMAND_ABORT:
-            return 'red';
-        }
-    }
-    return parent::getColor();
-  }
 }
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php b/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php
--- a/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php
@@ -1,10 +1,7 @@
 <?php
 
 final class HarbormasterBuildableTransaction
-  extends PhabricatorApplicationTransaction {
-
-  const TYPE_CREATE = 'harbormaster:buildable:create';
-  const TYPE_COMMAND = 'harbormaster:buildable:command';
+  extends PhabricatorModularTransaction {
 
   public function getApplicationName() {
     return 'harbormaster';
@@ -14,74 +11,8 @@
     return HarbormasterBuildablePHIDType::TYPECONST;
   }
 
-  public function getTitle() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CREATE:
-        return pht(
-          '%s created this buildable.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_COMMAND:
-        switch ($new) {
-          case HarbormasterBuildCommand::COMMAND_RESTART:
-            return pht(
-              '%s restarted this buildable.',
-              $this->renderHandleLink($author_phid));
-          case HarbormasterBuildCommand::COMMAND_RESUME:
-            return pht(
-              '%s resumed this buildable.',
-              $this->renderHandleLink($author_phid));
-          case HarbormasterBuildCommand::COMMAND_PAUSE:
-            return pht(
-              '%s paused this buildable.',
-              $this->renderHandleLink($author_phid));
-        }
-    }
-    return parent::getTitle();
+  public function getBaseTransactionClass() {
+    return 'HarbormasterBuildableTransactionType';
   }
 
-  public function getIcon() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CREATE:
-        return 'fa-plus';
-      case self::TYPE_COMMAND:
-        switch ($new) {
-          case HarbormasterBuildCommand::COMMAND_RESTART:
-            return 'fa-backward';
-          case HarbormasterBuildCommand::COMMAND_RESUME:
-            return 'fa-play';
-          case HarbormasterBuildCommand::COMMAND_PAUSE:
-            return 'fa-pause';
-        }
-    }
-
-    return parent::getIcon();
-  }
-
-  public function getColor() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CREATE:
-        return 'green';
-      case self::TYPE_COMMAND:
-        switch ($new) {
-          case HarbormasterBuildCommand::COMMAND_PAUSE:
-            return 'red';
-        }
-    }
-    return parent::getColor();
-  }
 }
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -18,7 +18,7 @@
   private $buildable = self::ATTACHABLE;
   private $buildPlan = self::ATTACHABLE;
   private $buildTargets = self::ATTACHABLE;
-  private $unprocessedCommands = self::ATTACHABLE;
+  private $unprocessedMessages = self::ATTACHABLE;
 
   public static function initializeNewBuild(PhabricatorUser $actor) {
     return id(new HarbormasterBuild())
@@ -28,7 +28,7 @@
 
   public function delete() {
     $this->openTransaction();
-      $this->deleteUnprocessedCommands();
+      $this->deleteUnprocessedMessages();
       $result = parent::delete();
     $this->saveTransaction();
 
@@ -207,11 +207,25 @@
     return $this->getBuildStatusObject()->isFailed();
   }
 
+  public function isPending() {
+    return $this->getBuildstatusObject()->isPending();
+  }
+
   public function getURI() {
     $id = $this->getID();
     return "/harbormaster/build/{$id}/";
   }
 
+  public function getBuildPendingStatusObject() {
+    list($pending_status) = $this->getUnprocessedMessageState();
+
+    if ($pending_status !== null) {
+      return HarbormasterBuildStatus::newBuildStatusObject($pending_status);
+    }
+
+    return $this->getBuildStatusObject();
+  }
+
   protected function getBuildStatusObject() {
     $status_key = $this->getBuildStatus();
     return HarbormasterBuildStatus::newBuildStatusObject($status_key);
@@ -222,263 +236,176 @@
   }
 
 
-/* -(  Build Commands  )----------------------------------------------------- */
+/* -(  Build Messages  )----------------------------------------------------- */
 
 
-  private function getUnprocessedCommands() {
-    return $this->assertAttached($this->unprocessedCommands);
+  private function getUnprocessedMessages() {
+    return $this->assertAttached($this->unprocessedMessages);
   }
 
-  public function attachUnprocessedCommands(array $commands) {
-    $this->unprocessedCommands = $commands;
-    return $this;
-  }
+  public function getUnprocessedMessagesForApply() {
+    $unprocessed_state = $this->getUnprocessedMessageState();
+    list($pending_status, $apply_messages) = $unprocessed_state;
 
-  public function canRestartBuild() {
-    try {
-      $this->assertCanRestartBuild();
-      return true;
-    } catch (HarbormasterRestartException $ex) {
-      return false;
-    }
+    return $apply_messages;
   }
 
-  public function assertCanRestartBuild() {
-    if ($this->isAutobuild()) {
-      throw new HarbormasterRestartException(
-        pht('Can Not Restart Autobuild'),
-        pht(
-          'This build can not be restarted because it is an automatic '.
-          'build.'));
-    }
+  private function getUnprocessedMessageState() {
+    // NOTE: If a build has multiple unprocessed messages, we'll ignore
+    // messages that are obsoleted by a later or stronger message.
+    //
+    // For example, if a build has both "pause" and "abort" messages in queue,
+    // we just ignore the "pause" message and perform an "abort", since pausing
+    // first wouldn't affect the final state, so we can just skip it.
+    //
+    // Likewise, if a build has both "restart" and "abort" messages, the most
+    // recent message is controlling: we'll take whichever action a command
+    // was most recently issued for.
 
-    $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE;
-    $plan = $this->getBuildPlan();
-
-    // See T13526. Users who can't see the "BuildPlan" can end up here with
-    // no object. This is highly questionable.
-    if (!$plan) {
-      throw new HarbormasterRestartException(
-        pht('No Build Plan Permission'),
-        pht(
-          'You can not restart this build because you do not have '.
-          'permission to access the build plan.'));
-    }
+    $is_restarting = false;
+    $is_aborting = false;
+    $is_pausing = false;
+    $is_resuming = false;
 
-    $option = HarbormasterBuildPlanBehavior::getBehavior($restartable)
-      ->getPlanOption($plan);
-    $option_key = $option->getKey();
-
-    $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER;
-    $is_never = ($option_key === $never_restartable);
-    if ($is_never) {
-      throw new HarbormasterRestartException(
-        pht('Build Plan Prevents Restart'),
-        pht(
-          'This build can not be restarted because the build plan is '.
-          'configured to prevent the build from restarting.'));
-    }
+    $apply_messages = array();
 
-    $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED;
-    $is_failed = ($option_key === $failed_restartable);
-    if ($is_failed) {
-      if (!$this->isFailed()) {
-        throw new HarbormasterRestartException(
-          pht('Only Restartable if Failed'),
-          pht(
-            'This build can not be restarted because the build plan is '.
-            'configured to prevent the build from restarting unless it '.
-            'has failed, and it has not failed.'));
+    foreach ($this->getUnprocessedMessages() as $message_object) {
+      $message_type = $message_object->getType();
+      switch ($message_type) {
+        case HarbormasterBuildMessageRestartTransaction::MESSAGETYPE:
+          $is_restarting = true;
+          $is_aborting = false;
+          $apply_messages = array($message_object);
+          break;
+        case HarbormasterBuildMessageAbortTransaction::MESSAGETYPE:
+          $is_aborting = true;
+          $is_restarting = false;
+          $apply_messages = array($message_object);
+          break;
+        case HarbormasterBuildMessagePauseTransaction::MESSAGETYPE:
+          $is_pausing = true;
+          $is_resuming = false;
+          $apply_messages = array($message_object);
+          break;
+        case HarbormasterBuildMessageResumeTransaction::MESSAGETYPE:
+          $is_resuming = true;
+          $is_pausing = false;
+          $apply_messages = array($message_object);
+          break;
       }
     }
 
-    if ($this->isRestarting()) {
-      throw new HarbormasterRestartException(
-        pht('Already Restarting'),
-        pht(
-          'This build is already restarting. You can not reissue a restart '.
-          'command to a restarting build.'));
-    }
-  }
-
-  public function canPauseBuild() {
-    if ($this->isAutobuild()) {
-      return false;
+    $pending_status = null;
+    if ($is_restarting) {
+      $pending_status = HarbormasterBuildStatus::PENDING_RESTARTING;
+    } else if ($is_aborting) {
+      $pending_status = HarbormasterBuildStatus::PENDING_ABORTING;
+    } else if ($is_pausing) {
+      $pending_status = HarbormasterBuildStatus::PENDING_PAUSING;
+    } else if ($is_resuming) {
+      $pending_status = HarbormasterBuildStatus::PENDING_RESUMING;
     }
 
-    return !$this->isComplete() &&
-           !$this->isPaused() &&
-           !$this->isPausing();
+    return array($pending_status, $apply_messages);
   }
 
-  public function canAbortBuild() {
-    if ($this->isAutobuild()) {
-      return false;
-    }
-
-    return !$this->isComplete();
-  }
-
-  public function canResumeBuild() {
-    if ($this->isAutobuild()) {
-      return false;
-    }
-
-    return $this->isPaused() &&
-           !$this->isResuming();
+  public function attachUnprocessedMessages(array $messages) {
+    assert_instances_of($messages, 'HarbormasterBuildMessage');
+    $this->unprocessedMessages = $messages;
+    return $this;
   }
 
   public function isPausing() {
-    $is_pausing = false;
-    foreach ($this->getUnprocessedCommands() as $command_object) {
-      $command = $command_object->getCommand();
-      switch ($command) {
-        case HarbormasterBuildCommand::COMMAND_PAUSE:
-          $is_pausing = true;
-          break;
-        case HarbormasterBuildCommand::COMMAND_RESUME:
-        case HarbormasterBuildCommand::COMMAND_RESTART:
-          $is_pausing = false;
-          break;
-        case HarbormasterBuildCommand::COMMAND_ABORT:
-          $is_pausing = true;
-          break;
-      }
-    }
-
-    return $is_pausing;
+    return $this->getBuildPendingStatusObject()->isPausing();
   }
 
   public function isResuming() {
-    $is_resuming = false;
-    foreach ($this->getUnprocessedCommands() as $command_object) {
-      $command = $command_object->getCommand();
-      switch ($command) {
-        case HarbormasterBuildCommand::COMMAND_RESTART:
-        case HarbormasterBuildCommand::COMMAND_RESUME:
-          $is_resuming = true;
-          break;
-        case HarbormasterBuildCommand::COMMAND_PAUSE:
-          $is_resuming = false;
-          break;
-        case HarbormasterBuildCommand::COMMAND_ABORT:
-          $is_resuming = false;
-          break;
-      }
-    }
-
-    return $is_resuming;
+    return $this->getBuildPendingStatusObject()->isResuming();
   }
 
   public function isRestarting() {
-    $is_restarting = false;
-    foreach ($this->getUnprocessedCommands() as $command_object) {
-      $command = $command_object->getCommand();
-      switch ($command) {
-        case HarbormasterBuildCommand::COMMAND_RESTART:
-          $is_restarting = true;
-          break;
-      }
-    }
-
-    return $is_restarting;
+    return $this->getBuildPendingStatusObject()->isRestarting();
   }
 
   public function isAborting() {
-    $is_aborting = false;
-    foreach ($this->getUnprocessedCommands() as $command_object) {
-      $command = $command_object->getCommand();
-      switch ($command) {
-        case HarbormasterBuildCommand::COMMAND_ABORT:
-          $is_aborting = true;
-          break;
-      }
+    return $this->getBuildPendingStatusObject()->isAborting();
+  }
+
+  public function markUnprocessedMessagesAsProcessed() {
+    foreach ($this->getUnprocessedMessages() as $key => $message_object) {
+      $message_object
+        ->setIsConsumed(1)
+        ->save();
     }
 
-    return $is_aborting;
+    return $this;
   }
 
-  public function deleteUnprocessedCommands() {
-    foreach ($this->getUnprocessedCommands() as $key => $command_object) {
-      $command_object->delete();
-      unset($this->unprocessedCommands[$key]);
+  public function deleteUnprocessedMessages() {
+    foreach ($this->getUnprocessedMessages() as $key => $message_object) {
+      $message_object->delete();
+      unset($this->unprocessedMessages[$key]);
     }
 
     return $this;
   }
 
-  public function canIssueCommand(PhabricatorUser $viewer, $command) {
-    try {
-      $this->assertCanIssueCommand($viewer, $command);
-      return true;
-    } catch (Exception $ex) {
-      return false;
-    }
+  public function sendMessage(PhabricatorUser $viewer, $message_type) {
+    HarbormasterBuildMessage::initializeNewMessage($viewer)
+      ->setReceiverPHID($this->getPHID())
+      ->setType($message_type)
+      ->save();
+
+    PhabricatorWorker::scheduleTask(
+      'HarbormasterBuildWorker',
+      array(
+        'buildID' => $this->getID(),
+      ),
+      array(
+        'objectPHID' => $this->getPHID(),
+        'containerPHID' => $this->getBuildablePHID(),
+      ));
   }
 
-  public function assertCanIssueCommand(PhabricatorUser $viewer, $command) {
-    $plan = $this->getBuildPlan();
+  public function releaseAllArtifacts(PhabricatorUser $viewer) {
+    $targets = id(new HarbormasterBuildTargetQuery())
+      ->setViewer($viewer)
+      ->withBuildPHIDs(array($this->getPHID()))
+      ->withBuildGenerations(array($this->getBuildGeneration()))
+      ->execute();
 
-    // See T13526. Users without permission to access the build plan can
-    // currently end up here with no "BuildPlan" object.
-    if (!$plan) {
-      return false;
+    if (!$targets) {
+      return;
     }
 
-    $need_edit = true;
-    switch ($command) {
-      case HarbormasterBuildCommand::COMMAND_RESTART:
-      case HarbormasterBuildCommand::COMMAND_PAUSE:
-      case HarbormasterBuildCommand::COMMAND_RESUME:
-      case HarbormasterBuildCommand::COMMAND_ABORT:
-        if ($plan->canRunWithoutEditCapability()) {
-          $need_edit = false;
-        }
-        break;
-      default:
-        throw new Exception(
-          pht(
-            'Invalid Harbormaster build command "%s".',
-            $command));
-    }
+    $target_phids = mpull($targets, 'getPHID');
 
-    // Issuing these commands requires that you be able to edit the build, to
-    // prevent enemy engineers from sabotaging your builds. See T9614.
-    if ($need_edit) {
-      PhabricatorPolicyFilter::requireCapability(
-        $viewer,
-        $plan,
-        PhabricatorPolicyCapability::CAN_EDIT);
+    $artifacts = id(new HarbormasterBuildArtifactQuery())
+      ->setViewer($viewer)
+      ->withBuildTargetPHIDs($target_phids)
+      ->withIsReleased(false)
+      ->execute();
+    foreach ($artifacts as $artifact) {
+      $artifact->releaseArtifact();
     }
   }
 
-  public function sendMessage(PhabricatorUser $viewer, $command) {
-    // TODO: This should not be an editor transaction, but there are plans to
-    // merge BuildCommand into BuildMessage which should moot this. As this
-    // exists today, it can race against BuildEngine.
+  public function restartBuild(PhabricatorUser $viewer) {
+    // TODO: This should become transactional.
 
-    // This is a bogus content source, but this whole flow should be obsolete
-    // soon.
-    $content_source = PhabricatorContentSource::newForSource(
-      PhabricatorConsoleContentSource::SOURCECONST);
+    // We're restarting the build, so release all previous artifacts.
+    $this->releaseAllArtifacts($viewer);
 
-    $editor = id(new HarbormasterBuildTransactionEditor())
-      ->setActor($viewer)
-      ->setContentSource($content_source)
-      ->setContinueOnNoEffect(true)
-      ->setContinueOnMissingFields(true);
-
-    $viewer_phid = $viewer->getPHID();
-    if (!$viewer_phid) {
-      $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
-      $editor->setActingAsPHID($acting_phid);
-    }
+    // Increment the build generation counter on the build.
+    $this->setBuildGeneration($this->getBuildGeneration() + 1);
 
-    $xaction = id(new HarbormasterBuildTransaction())
-      ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
-      ->setNewValue($command);
+    // Currently running targets should periodically check their build
+    // generation (which won't have changed) against the build's generation.
+    // If it is different, they will automatically stop what they're doing
+    // and abort.
 
-    $editor->applyTransactions($this, array($xaction));
+    // Previously we used to delete targets, logs and artifacts here. Instead,
+    // leave them around so users can view previous generations of this build.
   }
 
 
diff --git a/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php b/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php
--- a/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php
+++ b/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php
@@ -5,7 +5,6 @@
   private $buildable;
   private $messages;
   private $limit;
-  private $excuse;
   private $showViewAll;
 
   public function setBuildable(HarbormasterBuildable $buildable) {
@@ -23,11 +22,6 @@
     return $this;
   }
 
-  public function setExcuse($excuse) {
-    $this->excuse = $excuse;
-    return $this;
-  }
-
   public function setShowViewAll($show_view_all) {
     $this->showViewAll = $show_view_all;
     return $this;
@@ -88,21 +82,6 @@
       $table->setLimit($this->limit);
     }
 
-    $excuse = $this->excuse;
-    if (strlen($excuse)) {
-      $excuse_icon = id(new PHUIIconView())
-        ->setIcon('fa-commenting-o red');
-
-      $table->setNotice(
-        array(
-          $excuse_icon,
-          ' ',
-          phutil_tag('strong', array(), pht('Excuse:')),
-          ' ',
-          $excuse,
-        ));
-    }
-
     $box->setTable($table);
 
     return $box;
diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php
@@ -0,0 +1,117 @@
+<?php
+
+final class HarbormasterBuildMessageAbortTransaction
+  extends HarbormasterBuildMessageTransaction {
+
+  const TRANSACTIONTYPE = 'message/abort';
+  const MESSAGETYPE = 'abort';
+
+  public function getHarbormasterBuildMessageName() {
+    return pht('Abort Build');
+  }
+
+  public function getHarbormasterBuildableMessageName() {
+    return pht('Abort Builds');
+  }
+
+  public function newConfirmPromptTitle() {
+    return pht('Really abort build?');
+  }
+
+  public function getHarbormasterBuildableMessageEffect() {
+    return pht('Build will abort.');
+  }
+
+  public function newConfirmPromptBody() {
+    return pht(
+      'Progress on this build will be discarded. Really abort build?');
+  }
+
+  public function getHarbormasterBuildMessageDescription() {
+    return pht('Abort the build, discarding progress.');
+  }
+
+  public function newBuildableConfirmPromptTitle(
+    array $builds,
+    array $sendable) {
+    return pht(
+      'Really abort %s build(s)?',
+      phutil_count($builds));
+  }
+
+  public function newBuildableConfirmPromptBody(
+    array $builds,
+    array $sendable) {
+
+    if (count($sendable) === count($builds)) {
+      return pht(
+        'If you abort all builds, work will halt immediately. Work '.
+        'will be discarded, and builds must be completely restarted.');
+    } else {
+      return pht(
+        'You can only abort some builds. Work will halt immediately on '.
+        'builds you can abort. Progress will be discarded, and builds must '.
+        'be completely restarted if you want them to complete.');
+    }
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s aborted this build.',
+      $this->renderAuthor());
+  }
+
+  public function getIcon() {
+    return 'fa-exclamation-triangle';
+  }
+
+  public function getColor() {
+    return 'red';
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $actor = $this->getActor();
+    $build = $object;
+
+    $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED);
+  }
+
+  public function applyExternalEffects($object, $value) {
+    $actor = $this->getActor();
+    $build = $object;
+
+    $build->releaseAllArtifacts($actor);
+  }
+
+  protected function newCanApplyMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isAutobuild()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Abort Build'),
+        pht(
+          'You can not abort a build that uses an autoplan.'));
+    }
+
+    if ($build->isComplete()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Abort Build'),
+        pht(
+          'You can not abort this biuld because it is already complete.'));
+    }
+  }
+
+  protected function newCanSendMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isAborting()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Abort Build'),
+        pht(
+          'You can not abort this build because it is already aborting.'));
+    }
+  }
+
+}
diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php
@@ -0,0 +1,126 @@
+<?php
+
+final class HarbormasterBuildMessagePauseTransaction
+  extends HarbormasterBuildMessageTransaction {
+
+  const TRANSACTIONTYPE = 'message/pause';
+  const MESSAGETYPE = 'pause';
+
+  public function getHarbormasterBuildMessageName() {
+    return pht('Pause Build');
+  }
+
+  public function getHarbormasterBuildableMessageName() {
+    return pht('Pause Builds');
+  }
+
+  public function newConfirmPromptTitle() {
+    return pht('Really pause build?');
+  }
+
+  public function getHarbormasterBuildableMessageEffect() {
+    return pht('Build will pause.');
+  }
+
+  public function newConfirmPromptBody() {
+    return pht(
+      'If you pause this build, work will halt once the current steps '.
+      'complete. You can resume the build later.');
+  }
+
+
+  public function getHarbormasterBuildMessageDescription() {
+    return pht('Pause the build.');
+  }
+
+  public function newBuildableConfirmPromptTitle(
+    array $builds,
+    array $sendable) {
+    return pht(
+      'Really pause %s build(s)?',
+      phutil_count($builds));
+  }
+
+  public function newBuildableConfirmPromptBody(
+    array $builds,
+    array $sendable) {
+
+    if (count($sendable) === count($builds)) {
+      return pht(
+        'If you pause all builds, work will halt once the current steps '.
+        'complete. You can resume the builds later.');
+    } else {
+      return pht(
+        'You can only pause some builds. Once the current steps complete, '.
+        'work will halt on builds you can pause. You can resume the builds '.
+        'later.');
+    }
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s paused this build.',
+      $this->renderAuthor());
+  }
+
+  public function getIcon() {
+    return 'fa-pause';
+  }
+
+  public function getColor() {
+    return 'red';
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $actor = $this->getActor();
+    $build = $object;
+
+    $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED);
+  }
+
+  protected function newCanApplyMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isAutobuild()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Pause Build'),
+        pht('You can not pause a build that uses an autoplan.'));
+    }
+
+    if ($build->isPaused()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Pause Build'),
+        pht('You can not pause this build because it is already paused.'));
+    }
+
+    if ($build->isComplete()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Pause Build'),
+        pht('You can not pause this build because it has already completed.'));
+    }
+  }
+
+  protected function newCanSendMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isPausing()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Pause Build'),
+        pht('You can not pause this build because it is already pausing.'));
+    }
+
+    if ($build->isRestarting()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Pause Build'),
+        pht('You can not pause this build because it is already restarting.'));
+    }
+
+    if ($build->isAborting()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Pause Build'),
+        pht('You can not pause this build because it is already aborting.'));
+    }
+  }
+}
diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php
@@ -0,0 +1,171 @@
+<?php
+
+final class HarbormasterBuildMessageRestartTransaction
+  extends HarbormasterBuildMessageTransaction {
+
+  const TRANSACTIONTYPE = 'message/restart';
+  const MESSAGETYPE = 'restart';
+
+  public function getHarbormasterBuildMessageName() {
+    return pht('Restart Build');
+  }
+
+  public function getHarbormasterBuildableMessageName() {
+    return pht('Restart Builds');
+  }
+
+  public function getHarbormasterBuildableMessageEffect() {
+    return pht('Build will restart.');
+  }
+
+  public function newConfirmPromptTitle() {
+    return pht('Really restart build?');
+  }
+
+  public function newConfirmPromptBody() {
+    return pht(
+      'Progress on this build will be discarded and the build will restart. '.
+      'Side effects of the build will occur again. Really restart build?');
+  }
+
+
+  public function getHarbormasterBuildMessageDescription() {
+    return pht('Restart the build, discarding all progress.');
+  }
+
+  public function newBuildableConfirmPromptTitle(
+    array $builds,
+    array $sendable) {
+    return pht(
+      'Really restart %s build(s)?',
+      phutil_count($builds));
+  }
+
+  public function newBuildableConfirmPromptBody(
+    array $builds,
+    array $sendable) {
+
+    if (count($sendable) === count($builds)) {
+      return pht(
+        'All builds will restart.');
+    } else {
+      return pht(
+        'You can only restart some builds.');
+    }
+  }
+
+  public function newBuildableConfirmPromptWarnings(
+    array $builds,
+    array $sendable) {
+
+    $building = false;
+    foreach ($sendable as $build) {
+      if ($build->isBuilding()) {
+        $building = true;
+        break;
+      }
+    }
+
+    $warnings = array();
+
+    if ($building) {
+      $warnings[] = pht(
+        'Progress on running builds will be discarded.');
+    }
+
+    if ($sendable) {
+      $warnings[] = pht(
+        'When a build is restarted, side effects associated with '.
+        'the build may occur again.');
+    }
+
+    return $warnings;
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s restarted this build.',
+      $this->renderAuthor());
+  }
+
+  public function getIcon() {
+    return 'fa-repeat';
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $actor = $this->getActor();
+    $build = $object;
+
+    $build->restartBuild($actor);
+    $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
+  }
+
+  protected function newCanApplyMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isAutobuild()) {
+      throw new HarbormasterMessageException(
+        pht('Can Not Restart Autobuild'),
+        pht(
+          'This build can not be restarted because it is an automatic '.
+          'build.'));
+    }
+
+    $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE;
+    $plan = $build->getBuildPlan();
+
+    // See T13526. Users who can't see the "BuildPlan" can end up here with
+    // no object. This is highly questionable.
+    if (!$plan) {
+      throw new HarbormasterMessageException(
+        pht('No Build Plan Permission'),
+        pht(
+          'You can not restart this build because you do not have '.
+          'permission to access the build plan.'));
+    }
+
+    $option = HarbormasterBuildPlanBehavior::getBehavior($restartable)
+      ->getPlanOption($plan);
+    $option_key = $option->getKey();
+
+    $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER;
+    $is_never = ($option_key === $never_restartable);
+    if ($is_never) {
+      throw new HarbormasterMessageException(
+        pht('Build Plan Prevents Restart'),
+        pht(
+          'This build can not be restarted because the build plan is '.
+          'configured to prevent the build from restarting.'));
+    }
+
+    $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED;
+    $is_failed = ($option_key === $failed_restartable);
+    if ($is_failed) {
+      if (!$this->isFailed()) {
+        throw new HarbormasterMessageException(
+          pht('Only Restartable if Failed'),
+          pht(
+            'This build can not be restarted because the build plan is '.
+            'configured to prevent the build from restarting unless it '.
+            'has failed, and it has not failed.'));
+      }
+    }
+
+  }
+
+  protected function newCanSendMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isRestarting()) {
+      throw new HarbormasterMessageException(
+        pht('Already Restarting'),
+        pht(
+          'This build is already restarting. You can not reissue a restart '.
+          'command to a restarting build.'));
+    }
+
+  }
+
+}
diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php
@@ -0,0 +1,119 @@
+<?php
+
+final class HarbormasterBuildMessageResumeTransaction
+  extends HarbormasterBuildMessageTransaction {
+
+  const TRANSACTIONTYPE = 'message/resume';
+  const MESSAGETYPE = 'resume';
+
+  public function getHarbormasterBuildMessageName() {
+    return pht('Resume Build');
+  }
+
+  public function getHarbormasterBuildableMessageName() {
+    return pht('Resume Builds');
+  }
+
+  public function getHarbormasterBuildableMessageEffect() {
+    return pht('Build will resume.');
+  }
+
+  public function newConfirmPromptTitle() {
+    return pht('Really resume build?');
+  }
+
+  public function newConfirmPromptBody() {
+    return pht(
+      'Work will continue on the build. Really resume?');
+  }
+
+  public function getHarbormasterBuildMessageDescription() {
+    return pht('Resume work on a previously paused build.');
+  }
+
+  public function newBuildableConfirmPromptTitle(
+    array $builds,
+    array $sendable) {
+    return pht(
+      'Really resume %s build(s)?',
+      phutil_count($builds));
+  }
+
+  public function newBuildableConfirmPromptBody(
+    array $builds,
+    array $sendable) {
+
+    if (count($sendable) === count($builds)) {
+      return pht(
+        'Work will continue on all builds. Really resume?');
+    } else {
+      return pht(
+        'You can only resume some builds. Work will continue on builds '.
+        'you have permission to resume.');
+    }
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s resumed this build.',
+      $this->renderAuthor());
+  }
+
+  public function getIcon() {
+    return 'fa-play';
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $actor = $this->getActor();
+    $build = $object;
+
+    $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
+  }
+
+  protected function newCanApplyMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isAutobuild()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Resume Build'),
+        pht(
+          'You can not resume a build that uses an autoplan.'));
+    }
+
+    if (!$build->isPaused() && !$build->isPausing()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Resume Build'),
+        pht(
+          'You can not resume this build because it is not paused. You can '.
+          'only resume a paused build.'));
+    }
+
+  }
+
+  protected function newCanSendMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    if ($build->isResuming()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Resume Build'),
+        pht(
+          'You can not resume this build beacuse it is already resuming.'));
+    }
+
+    if ($build->isRestarting()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Resume Build'),
+        pht('You can not resume this build because it is already restarting.'));
+    }
+
+    if ($build->isAborting()) {
+      throw new HarbormasterMessageException(
+        pht('Unable to Resume Build'),
+        pht('You can not resume this build because it is already aborting.'));
+    }
+
+  }
+
+}
diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php
@@ -0,0 +1,158 @@
+<?php
+
+abstract class HarbormasterBuildMessageTransaction
+  extends HarbormasterBuildTransactionType {
+
+  final public function getHarbormasterBuildMessageType() {
+    return $this->getPhobjectClassConstant('MESSAGETYPE');
+  }
+
+  abstract public function getHarbormasterBuildMessageName();
+  abstract public function getHarbormasterBuildMessageDescription();
+  abstract public function getHarbormasterBuildableMessageName();
+  abstract public function getHarbormasterBuildableMessageEffect();
+
+  abstract public function newConfirmPromptTitle();
+  abstract public function newConfirmPromptBody();
+
+  abstract public function newBuildableConfirmPromptTitle(
+    array $builds,
+    array $sendable);
+
+  abstract public function newBuildableConfirmPromptBody(
+    array $builds,
+    array $sendable);
+
+  public function newBuildableConfirmPromptWarnings(
+    array $builds,
+    array $sendable) {
+    return array();
+  }
+
+  final public function generateOldValue($object) {
+    return null;
+  }
+
+  final public function getTransactionTypeForConduit($xaction) {
+    return 'message';
+  }
+
+  final public function getFieldValuesForConduit($xaction, $data) {
+    return array(
+      'type' => $xaction->getNewValue(),
+    );
+  }
+
+  final public static function getAllMessages() {
+    $message_xactions = id(new PhutilClassMapQuery())
+      ->setAncestorClass(__CLASS__)
+      ->execute();
+
+    return $message_xactions;
+  }
+
+  final public static function getTransactionObjectForMessageType(
+    $message_type) {
+    $message_xactions = self::getAllMessages();
+
+    foreach ($message_xactions as $message_xaction) {
+      $xaction_type = $message_xaction->getHarbormasterBuildMessageType();
+      if ($xaction_type === $message_type) {
+        return $message_xaction;
+      }
+    }
+
+    return null;
+  }
+
+  final public static function getTransactionTypeForMessageType($message_type) {
+    $message_xaction = self::getTransactionObjectForMessageType($message_type);
+
+    if ($message_xaction) {
+      return $message_xaction->getTransactionTypeConstant();
+    }
+
+    return null;
+  }
+
+  final public function getTransactionHasEffect($object, $old, $new) {
+    return $this->canApplyMessage($this->getActor(), $object);
+  }
+
+  final public function canApplyMessage(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    try {
+      $this->assertCanApplyMessage($viewer, $build);
+      return true;
+    } catch (HarbormasterMessageException $ex) {
+      return false;
+    }
+  }
+
+  final public function canSendMessage(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+
+    try {
+      $this->assertCanSendMessage($viewer, $build);
+      return true;
+    } catch (HarbormasterMessageException $ex) {
+      return false;
+    }
+  }
+
+  final public function assertCanApplyMessage(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+    $this->newCanApplyMessageAssertion($viewer, $build);
+  }
+
+  final public function assertCanSendMessage(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build) {
+    $plan = $build->getBuildPlan();
+
+    // See T13526. Users without permission to access the build plan can
+    // currently end up here with no "BuildPlan" object.
+    if (!$plan) {
+      throw new HarbormasterMessageException(
+        pht('No Build Plan Permission'),
+        pht(
+          'You can not issue this command because you do not have '.
+          'permission to access the build plan for this build.'));
+    }
+
+    // Issuing these commands requires that you be able to edit the build, to
+    // prevent enemy engineers from sabotaging your builds. See T9614.
+    if (!$plan->canRunWithoutEditCapability()) {
+      try {
+        PhabricatorPolicyFilter::requireCapability(
+          $viewer,
+          $plan,
+          PhabricatorPolicyCapability::CAN_EDIT);
+      } catch (PhabricatorPolicyException $ex) {
+        throw new HarbormasterMessageException(
+          pht('Insufficent Build Plan Permission'),
+          pht(
+            'The build plan for this build is configured to prevent '.
+            'users who can not edit it from issuing commands to the '.
+            'build, and you do not have permission to edit the build '.
+            'plan.'));
+      }
+    }
+
+    $this->newCanSendMessageAssertion($viewer, $build);
+    $this->assertCanApplyMessage($viewer, $build);
+  }
+
+  abstract protected function newCanSendMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build);
+
+  abstract protected function newCanApplyMessageAssertion(
+    PhabricatorUser $viewer,
+    HarbormasterBuild $build);
+
+}
diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildTransactionType.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class HarbormasterBuildTransactionType
+  extends PhabricatorModularTransactionType {}
diff --git a/src/applications/harbormaster/xaction/buildable/HarbormasterBuildableMessageTransaction.php b/src/applications/harbormaster/xaction/buildable/HarbormasterBuildableMessageTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/buildable/HarbormasterBuildableMessageTransaction.php
@@ -0,0 +1,65 @@
+<?php
+
+final class HarbormasterBuildableMessageTransaction
+  extends HarbormasterBuildableTransactionType {
+
+  const TRANSACTIONTYPE = 'harbormaster:buildable:command';
+
+  public function generateOldValue($object) {
+    return null;
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+
+    switch ($new) {
+      case HarbormasterBuildMessageRestartTransaction::MESSAGETYPE:
+        return pht(
+          '%s restarted this buildable.',
+          $this->renderAuthor());
+      case HarbormasterBuildMessageResumeTransaction::MESSAGETYPE:
+        return pht(
+          '%s resumed this buildable.',
+          $this->renderAuthor());
+      case HarbormasterBuildMessagePauseTransaction::MESSAGETYPE:
+        return pht(
+          '%s paused this buildable.',
+          $this->renderAuthor());
+      case HarbormasterBuildMessageAbortTransaction::MESSAGETYPE:
+        return pht(
+          '%s aborted this buildable.',
+          $this->renderAuthor());
+    }
+
+    return parent::getTitle();
+  }
+
+  public function getIcon() {
+    $new = $this->getNewValue();
+
+    switch ($new) {
+      case HarbormasterBuildMessageRestartTransaction::MESSAGETYPE:
+        return 'fa-backward';
+      case HarbormasterBuildMessageResumeTransaction::MESSAGETYPE:
+        return 'fa-play';
+      case HarbormasterBuildMessagePauseTransaction::MESSAGETYPE:
+        return 'fa-pause';
+      case HarbormasterBuildMessageAbortTransaction::MESSAGETYPE:
+        return 'fa-exclamation-triangle';
+    }
+
+    return parent::getIcon();
+  }
+
+  public function getColor() {
+    $new = $this->getNewValue();
+
+    switch ($new) {
+      case HarbormasterBuildMessagePauseTransaction::MESSAGETYPE:
+        return 'red';
+    }
+
+    return parent::getColor();
+  }
+
+}
diff --git a/src/applications/harbormaster/xaction/buildable/HarbormasterBuildableTransactionType.php b/src/applications/harbormaster/xaction/buildable/HarbormasterBuildableTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/buildable/HarbormasterBuildableTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class HarbormasterBuildableTransactionType
+  extends PhabricatorModularTransactionType {}
diff --git a/src/applications/meta/query/PhabricatorApplicationQuery.php b/src/applications/meta/query/PhabricatorApplicationQuery.php
--- a/src/applications/meta/query/PhabricatorApplicationQuery.php
+++ b/src/applications/meta/query/PhabricatorApplicationQuery.php
@@ -89,7 +89,7 @@
       }
     }
 
-    if (strlen($this->nameContains)) {
+    if ($this->nameContains !== null) {
       foreach ($apps as $key => $app) {
         if (stripos($app->getName(), $this->nameContains) === false) {
           unset($apps[$key]);
diff --git a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
--- a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
@@ -33,10 +33,11 @@
         //
         // "Configuring Outbound Email" should be updated if this changes.
         //
-        // These addresses were last updated in January 2019.
+        // These addresses were last updated in December 2021.
         '50.31.156.6/32',
         '50.31.156.77/32',
         '18.217.206.57/32',
+        '3.134.147.250/32',
       ),
     );
   }
diff --git a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
--- a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
+++ b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
@@ -11,7 +11,7 @@
    *
    * Or
    *
-   *   !assign epriestley
+   *   !assign alincoln
    *
    *   please, take this task I took; its hard
    *
@@ -20,9 +20,9 @@
    * commands. For example, this body above might parse as:
    *
    *   array(
-   *     'body' => 'please, take this task I took; its hard',
+   *     'body' => 'please, take this task I took; it's hard',
    *     'commands' => array(
-   *       array('assign', 'epriestley'),
+   *       array('assign', 'alincoln'),
    *     ),
    *   )
    *
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php
--- a/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php
@@ -60,6 +60,16 @@
         }
         $package_map[$package->getPHID()] = $package_owners;
       }
+
+      // See T13648. We may have packages that no longer exist or can't be
+      // loaded (for example, because they have been destroyed). Give them
+      // empty entries in the map so we return a mapping for all input PHIDs.
+
+      foreach ($package_phids as $package_phid) {
+        if (!isset($package_map[$package_phid])) {
+          $package_map[$package_phid] = array();
+        }
+      }
     }
 
     $results = array();
diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
--- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
@@ -197,6 +197,12 @@
     $name = idx($spec, 'short', $dominion);
     $view->addProperty(pht('Dominion'), $name);
 
+    $authority_mode = $package->getAuthorityMode();
+    $authority_map = PhabricatorOwnersPackage::getAuthorityOptionsMap();
+    $spec = idx($authority_map, $authority_mode, array());
+    $name = idx($spec, 'short', $authority_mode);
+    $view->addProperty(pht('Authority'), $name);
+
     $auto = $package->getAutoReview();
     $autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
     $spec = idx($autoreview_map, $auto, array());
diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
--- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
+++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
@@ -90,6 +90,9 @@
     $dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap();
     $dominion_map = ipull($dominion_map, 'name');
 
+    $authority_map = PhabricatorOwnersPackage::getAuthorityOptionsMap();
+    $authority_map = ipull($authority_map, 'name');
+
     return array(
       id(new PhabricatorTextEditField())
         ->setKey('name')
@@ -118,6 +121,16 @@
         ->setIsCopyable(true)
         ->setValue($object->getDominion())
         ->setOptions($dominion_map),
+      id(new PhabricatorSelectEditField())
+        ->setKey('authority')
+        ->setLabel(pht('Authority'))
+        ->setDescription(
+          pht('Change package authority rules.'))
+        ->setTransactionType(
+          PhabricatorOwnersPackageAuthorityTransaction::TRANSACTIONTYPE)
+        ->setIsCopyable(true)
+        ->setValue($object->getAuthorityMode())
+        ->setOptions($authority_map),
       id(new PhabricatorSelectEditField())
         ->setKey('autoReview')
         ->setLabel(pht('Auto Review'))
diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
--- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
+++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
@@ -10,6 +10,7 @@
   private $repositoryPHIDs;
   private $paths;
   private $statuses;
+  private $authorityModes;
 
   private $controlMap = array();
   private $controlResults;
@@ -77,6 +78,11 @@
     return $this;
   }
 
+  public function withAuthorityModes(array $modes) {
+    $this->authorityModes = $modes;
+    return $this;
+  }
+
   public function withNameNgrams($ngrams) {
     return $this->withNgramsConstraint(
       new PhabricatorOwnersPackageNameNgrams(),
@@ -231,6 +237,13 @@
       $where[] = qsprintf($conn, '%LO', $clauses);
     }
 
+    if ($this->authorityModes !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'authorityMode IN (%Ls)',
+        $this->authorityModes);
+    }
+
     return $where;
   }
 
diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php
--- a/src/applications/owners/storage/PhabricatorOwnersPackage.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php
@@ -21,6 +21,7 @@
   protected $dominion;
   protected $properties = array();
   protected $auditingState;
+  protected $authorityMode;
 
   private $paths = self::ATTACHABLE;
   private $owners = self::ATTACHABLE;
@@ -41,6 +42,9 @@
   const DOMINION_STRONG = 'strong';
   const DOMINION_WEAK = 'weak';
 
+  const AUTHORITY_STRONG = 'strong';
+  const AUTHORITY_WEAK = 'weak';
+
   const PROPERTY_IGNORED = 'ignored';
 
   public static function initializeNewPackage(PhabricatorUser $actor) {
@@ -58,6 +62,7 @@
       ->setAuditingState(PhabricatorOwnersAuditRule::AUDITING_NONE)
       ->setAutoReview(self::AUTOREVIEW_NONE)
       ->setDominion(self::DOMINION_STRONG)
+      ->setAuthorityMode(self::AUTHORITY_STRONG)
       ->setViewPolicy($view_policy)
       ->setEditPolicy($edit_policy)
       ->attachPaths(array())
@@ -115,6 +120,19 @@
     );
   }
 
+  public static function getAuthorityOptionsMap() {
+    return array(
+      self::AUTHORITY_STRONG => array(
+        'name' => pht('Strong (Package Owns Paths)'),
+        'short' => pht('Strong'),
+      ),
+      self::AUTHORITY_WEAK => array(
+        'name' => pht('Weak (Package Watches Paths)'),
+        'short' => pht('Weak'),
+      ),
+    );
+  }
+
   protected function getConfiguration() {
     return array(
       // This information is better available from the history table.
@@ -130,6 +148,7 @@
         'status' => 'text32',
         'autoReview' => 'text32',
         'dominion' => 'text32',
+        'authorityMode' => 'text32',
       ),
     ) + parent::getConfiguration();
   }
@@ -568,6 +587,10 @@
     return PhabricatorOwnersAuditRule::newFromState($this->getAuditingState());
   }
 
+  public function getHasStrongAuthority() {
+    return ($this->getAuthorityMode() === self::AUTHORITY_STRONG);
+  }
+
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
@@ -696,6 +719,10 @@
         ->setKey('dominion')
         ->setType('map<string, wild>')
         ->setDescription(pht('Dominion setting information.')),
+      id(new PhabricatorConduitSearchFieldSpecification())
+        ->setKey('authority')
+        ->setType('map<string, wild>')
+        ->setDescription(pht('Authority setting information.')),
       id(new PhabricatorConduitSearchFieldSpecification())
         ->setKey('ignored')
         ->setType('map<string, wild>')
@@ -747,6 +774,23 @@
       'short' => $dominion_short,
     );
 
+
+    $authority_value = $this->getAuthorityMode();
+    $authority_map = self::getAuthorityOptionsMap();
+    if (isset($authority_map[$authority_value])) {
+      $authority_label = $authority_map[$authority_value]['name'];
+      $authority_short = $authority_map[$authority_value]['short'];
+    } else {
+      $authority_label = pht('Unknown ("%s")', $authority_value);
+      $authority_short = pht('Unknown ("%s")', $authority_value);
+    }
+
+    $authority = array(
+      'value' => $authority_value,
+      'label' => $authority_label,
+      'short' => $authority_short,
+    );
+
     // Force this to always emit as a JSON object even if empty, never as
     // a JSON list.
     $ignored = $this->getIgnoredPathAttributes();
@@ -762,6 +806,7 @@
       'review' => $review,
       'audit' => $audit,
       'dominion' => $dominion,
+      'authority' => $authority,
       'ignored' => $ignored,
     );
   }
diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageAuthorityTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageAuthorityTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/owners/xaction/PhabricatorOwnersPackageAuthorityTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhabricatorOwnersPackageAuthorityTransaction
+  extends PhabricatorOwnersPackageTransactionType {
+
+  const TRANSACTIONTYPE = 'owners.authority';
+
+  public function generateOldValue($object) {
+    return $object->getAuthorityMode();
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $map = PhabricatorOwnersPackage::getAuthorityOptionsMap();
+    foreach ($xactions as $xaction) {
+      $new = $xaction->getNewValue();
+
+      if (empty($map[$new])) {
+        $valid = array_keys($map);
+
+        $errors[] = $this->newInvalidError(
+          pht(
+            'Authority setting "%s" is not valid. '.
+            'Valid settings are: %s.',
+            $new,
+            implode(', ', $valid)),
+          $xaction);
+      }
+    }
+
+    return $errors;
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setAuthorityMode($value);
+  }
+
+  public function getTitle() {
+    $map = PhabricatorOwnersPackage::getAuthorityOptionsMap();
+    $map = ipull($map, 'short');
+
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    $old = idx($map, $old, $old);
+    $new = idx($map, $new, $new);
+
+    return pht(
+      '%s adjusted package authority rules from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderValue($old),
+      $this->renderValue($new));
+  }
+
+}
diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php
--- a/src/applications/people/query/PhabricatorPeopleQuery.php
+++ b/src/applications/people/query/PhabricatorPeopleQuery.php
@@ -341,7 +341,7 @@
         (int)$this->isMailingList);
     }
 
-    if (strlen($this->nameLike)) {
+    if ($this->nameLike !== null) {
       $where[] = qsprintf(
         $conn,
         'user.username LIKE %~ OR user.realname LIKE %~',
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -275,7 +275,8 @@
       $this->setConduitCertificate($this->generateConduitCertificate());
     }
 
-    if (!strlen($this->getAccountSecret())) {
+    $secret = $this->getAccountSecret();
+    if (($secret === null) || !strlen($secret)) {
       $this->setAccountSecret(Filesystem::readRandomCharacters(64));
     }
 
diff --git a/src/applications/phid/handle/pool/PhabricatorHandleList.php b/src/applications/phid/handle/pool/PhabricatorHandleList.php
--- a/src/applications/phid/handle/pool/PhabricatorHandleList.php
+++ b/src/applications/phid/handle/pool/PhabricatorHandleList.php
@@ -126,22 +126,27 @@
 /* -(  Iterator  )----------------------------------------------------------- */
 
 
+  #[\ReturnTypeWillChange]
   public function rewind() {
     $this->cursor = 0;
   }
 
+  #[\ReturnTypeWillChange]
   public function current() {
     return $this->getHandle($this->phids[$this->cursor]);
   }
 
+  #[\ReturnTypeWillChange]
   public function key() {
     return $this->phids[$this->cursor];
   }
 
+  #[\ReturnTypeWillChange]
   public function next() {
     ++$this->cursor;
   }
 
+  #[\ReturnTypeWillChange]
   public function valid() {
     return ($this->cursor < $this->count);
   }
@@ -150,6 +155,7 @@
 /* -(  ArrayAccess  )-------------------------------------------------------- */
 
 
+  #[\ReturnTypeWillChange]
   public function offsetExists($offset) {
     // NOTE: We're intentionally not loading handles here so that isset()
     // checks do not trigger fetches. This gives us better bulk loading
@@ -162,6 +168,7 @@
     return isset($this->map[$offset]);
   }
 
+  #[\ReturnTypeWillChange]
   public function offsetGet($offset) {
     if ($this->handles === null) {
       $this->loadHandles();
@@ -169,10 +176,12 @@
     return $this->handles[$offset];
   }
 
+  #[\ReturnTypeWillChange]
   public function offsetSet($offset, $value) {
     $this->raiseImmutableException();
   }
 
+  #[\ReturnTypeWillChange]
   public function offsetUnset($offset) {
     $this->raiseImmutableException();
   }
@@ -189,6 +198,7 @@
 /* -(  Countable  )---------------------------------------------------------- */
 
 
+  #[\ReturnTypeWillChange]
   public function count() {
     return $this->count;
   }
diff --git a/src/applications/phriction/application/PhabricatorPhrictionApplication.php b/src/applications/phriction/application/PhabricatorPhrictionApplication.php
--- a/src/applications/phriction/application/PhabricatorPhrictionApplication.php
+++ b/src/applications/phriction/application/PhabricatorPhrictionApplication.php
@@ -61,7 +61,7 @@
         'new/'                        => 'PhrictionNewController',
         'move/(?P<id>[1-9]\d*)/' => 'PhrictionMoveController',
 
-        'preview/(?P<slug>.*/)' => 'PhrictionMarkupPreviewController',
+        'preview/' => 'PhrictionMarkupPreviewController',
         'diff/(?P<id>[1-9]\d*)/' => 'PhrictionDiffController',
 
         $this->getEditRoutePattern('document/edit/')
diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php
--- a/src/applications/phriction/controller/PhrictionEditController.php
+++ b/src/applications/phriction/controller/PhrictionEditController.php
@@ -316,9 +316,17 @@
       ->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
       ->setForm($form);
 
+    $preview_uri = '/phriction/preview/';
+    $preview_uri = new PhutilURI(
+      $preview_uri,
+      array(
+        'slug' => $document->getSlug(),
+      ));
+    $preview_uri = phutil_string_cast($preview_uri);
+
     $preview = id(new PHUIRemarkupPreviewPanel())
       ->setHeader($content->getTitle())
-      ->setPreviewURI('/phriction/preview/'.$document->getSlug())
+      ->setPreviewURI($preview_uri)
       ->setControlID('document-textarea')
       ->setPreviewType(PHUIRemarkupPreviewPanel::DOCUMENT);
 
diff --git a/src/applications/phriction/controller/PhrictionMarkupPreviewController.php b/src/applications/phriction/controller/PhrictionMarkupPreviewController.php
--- a/src/applications/phriction/controller/PhrictionMarkupPreviewController.php
+++ b/src/applications/phriction/controller/PhrictionMarkupPreviewController.php
@@ -3,12 +3,29 @@
 final class PhrictionMarkupPreviewController
   extends PhabricatorController {
 
-  public function processRequest() {
-    $request = $this->getRequest();
-    $viewer = $request->getUser();
+  public function handleRequest(AphrontRequest $request) {
+    $viewer = $request->getViewer();
 
     $text = $request->getStr('text');
-    $slug = $request->getURIData('slug');
+    $slug = $request->getStr('slug');
+
+    $document = id(new PhrictionDocumentQuery())
+      ->setViewer($viewer)
+      ->withSlugs(array($slug))
+      ->needContent(true)
+      ->executeOne();
+    if (!$document) {
+      $document = PhrictionDocument::initializeNewDocument(
+        $viewer,
+        $slug);
+
+      $content = id(new PhrictionContent())
+        ->setSlug($slug);
+
+      $document
+        ->setPHID($document->generatePHID())
+        ->attachContent($content);
+    }
 
     $output = PhabricatorMarkupEngine::renderOneObject(
       id(new PhabricatorMarkupOneOff())
@@ -17,10 +34,7 @@
         ->setContent($text),
       'default',
       $viewer,
-      array(
-        'phriction.isPreview' => true,
-        'phriction.slug' => $slug,
-      ));
+      $document);
 
     return id(new AphrontAjaxResponse())
       ->setContent($output);
diff --git a/src/applications/phriction/markup/PhrictionRemarkupRule.php b/src/applications/phriction/markup/PhrictionRemarkupRule.php
--- a/src/applications/phriction/markup/PhrictionRemarkupRule.php
+++ b/src/applications/phriction/markup/PhrictionRemarkupRule.php
@@ -273,13 +273,6 @@
       return null;
     }
 
-    // Handle content when it's a preview for the Phriction editor.
-    if (is_array($context)) {
-      if (idx($context, 'phriction.isPreview')) {
-        return idx($context, 'phriction.slug');
-      }
-    }
-
     if ($context instanceof PhrictionContent) {
       return $context->getSlug();
     }
diff --git a/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php b/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php
--- a/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php
+++ b/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php
@@ -16,13 +16,23 @@
   private $local          = array();
   private $localParents   = array();
 
-  public function __construct(PhabricatorRepository $repository, $commit) {
+  public function __construct(PhabricatorRepository $repository,
+    $start_commit = null) {
+
     $this->repository = $repository;
 
+    $command = 'log --template %s --rev %s';
+    $template = '{rev}\1{node}\1{date}\1{parents}\2';
+    if ($start_commit !== null) {
+      $revset = hgsprintf('reverse(ancestors(%s))', $start_commit);
+    } else {
+      $revset = 'reverse(all())';
+    }
+
     $future = $repository->getLocalCommandFuture(
-      'log --template %s --rev %s',
-      '{rev}\1{node}\1{date}\1{parents}\2',
-      hgsprintf('reverse(ancestors(%s))', $commit));
+      $command,
+      $template,
+      $revset);
 
     $this->iterator = new LinesOfALargeExecFuture($future);
     $this->iterator->setDelimiter("\2");
diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php
--- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php
@@ -780,8 +780,7 @@
   }
 
   private function markUnreachableCommits(PhabricatorRepository $repository) {
-    // For now, this is only supported for Git.
-    if (!$repository->isGit()) {
+    if (!$repository->isGit() && !$repository->isHg()) {
       return;
     }
 
@@ -799,7 +798,11 @@
     }
 
     // We can share a single graph stream across all the checks we need to do.
-    $stream = new PhabricatorGitGraphStream($repository);
+    if ($repository->isGit()) {
+      $stream = new PhabricatorGitGraphStream($repository);
+    } else if ($repository->isHg()) {
+      $stream = new PhabricatorMercurialGraphStream($repository);
+    }
 
     foreach ($old_refs as $old_ref) {
       $identifier = $old_ref->getCommitIdentifier();
@@ -812,7 +815,7 @@
 
   private function markUnreachableFrom(
     PhabricatorRepository $repository,
-    PhabricatorGitGraphStream $stream,
+    PhabricatorRepositoryGraphStream $stream,
     $identifier) {
 
     $unreachable = array();
diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
--- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
@@ -723,7 +723,6 @@
       // This behavior has been reverted, but users who updated between Feb 1,
       // 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test
       // against stdout to check for this possibility.
-      // See: https://github.com/phacility/phabricator/issues/101/
 
       // NOTE: Mercurial has translated versions, which translate this error
       // string. In a translated version, the string will be something else,
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php
--- a/src/applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php
@@ -48,11 +48,11 @@
   }
 
   private function markReachable(PhabricatorRepository $repository) {
-    if (!$repository->isGit()) {
+    if (!$repository->isGit() && !$repository->isHg()) {
       throw new PhutilArgumentUsageException(
         pht(
-          'Only Git repositories are supported, this repository ("%s") is '.
-          'not a Git repository.',
+          'Only Git and Mercurial repositories are supported, unable to '.
+          'operate on this repository ("%s").',
           $repository->getDisplayName()));
     }
 
@@ -65,7 +65,12 @@
 
     $flag = PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE;
 
-    $graph = new PhabricatorGitGraphStream($repository);
+    if ($repository->isGit()) {
+      $graph = new PhabricatorGitGraphStream($repository);
+    } else if ($repository->isHg()) {
+      $graph = new PhabricatorMercurialGraphStream($repository);
+    }
+
     foreach ($commits as $commit) {
       $identifier = $commit->getCommitIdentifier();
 
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -499,7 +499,7 @@
 
   public function passthruRemoteCommand($pattern /* , $arg, ... */) {
     $args = func_get_args();
-    return $this->newRemoteCommandPassthru($args)->execute();
+    return $this->newRemoteCommandPassthru($args)->resolve();
   }
 
   private function newRemoteCommandFuture(array $argv) {
@@ -540,7 +540,7 @@
 
   public function passthruLocalCommand($pattern /* , $arg, ... */) {
     $args = func_get_args();
-    return $this->newLocalCommandPassthru($args)->execute();
+    return $this->newLocalCommandPassthru($args)->resolve();
   }
 
   private function newLocalCommandFuture(array $argv) {
@@ -2269,10 +2269,9 @@
       $never_proxy);
 
     if (!$client) {
-      $result = id(new ConduitCall($method, $params))
-        ->setUser($viewer)
-        ->execute();
-      $future = new ImmediateFuture($result);
+      $conduit_call = id(new ConduitCall($method, $params))
+        ->setUser($viewer);
+      $future = new MethodCallFuture($conduit_call, 'execute');
     } else {
       $future = $client->callMethod($method, $params);
     }
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -714,8 +714,8 @@
     if (!$path) {
       throw new Exception(
         pht(
-          'This commit ("%s") is associated with a repository ("%s") that '.
-          'with a remote URI ("%s") that does not appear to be hosted on '.
+          'This commit ("%s") is associated with a repository ("%s") which '.
+          'has a remote URI ("%s") that does not appear to be hosted on '.
           'GitHub. Repositories must be hosted on GitHub to be built with '.
           'CircleCI.',
           $commit_phid,
diff --git a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
--- a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
+++ b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
@@ -100,55 +100,6 @@
 
   }
 
-  public function testFilterMercurialDebugOutput() {
-    $map = array(
-      '' => '',
-
-      "quack\n" => "quack\n",
-
-      "ignoring untrusted configuration option x.y = z\nquack\n" =>
-        "quack\n",
-
-      "ignoring untrusted configuration option x.y = z\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "quack\n" =>
-        "quack\n",
-
-      "ignoring untrusted configuration option x.y = z\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "quack\n" =>
-        "quack\n",
-
-      "quack\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "ignoring untrusted configuration option x.y = z\n" =>
-        "quack\n",
-
-      "ignoring untrusted configuration option x.y = z\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "duck\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "bread\n".
-      "ignoring untrusted configuration option x.y = z\n".
-      "quack\n" =>
-        "duck\nbread\nquack\n",
-
-      "ignoring untrusted configuration option x.y = z\n".
-      "duckignoring untrusted configuration option x.y = z\n".
-      "quack" =>
-        'duckquack',
-    );
-
-    foreach ($map as $input => $expect) {
-      $actual = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
-        $input);
-      $this->assertEqual($expect, $actual, $input);
-    }
-  }
-
   public function testRepositoryShortNameValidation() {
     $good = array(
       'sensible-repository',
diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
--- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
+++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
@@ -60,7 +60,7 @@
       PhabricatorEnv::getDoclink('Conduit API: Using Search Endpoints'));
   }
 
-  final public function getMethodDocumentation() {
+  final protected function newDocumentationPages(PhabricatorUser $viewer) {
     $viewer = $this->getViewer();
 
     $engine = $this->newSearchEngine()
@@ -70,17 +70,18 @@
 
     $out = array();
 
-    $out[] = $this->buildQueriesBox($engine);
-    $out[] = $this->buildConstraintsBox($engine);
-    $out[] = $this->buildOrderBox($engine, $query);
-    $out[] = $this->buildFieldsBox($engine);
-    $out[] = $this->buildAttachmentsBox($engine);
-    $out[] = $this->buildPagingBox($engine);
+    $out[] = $this->buildQueriesDocumentationPage($viewer, $engine);
+    $out[] = $this->buildConstraintsDocumentationPage($viewer, $engine);
+    $out[] = $this->buildOrderDocumentationPage($viewer, $engine, $query);
+    $out[] = $this->buildFieldsDocumentationPage($viewer, $engine);
+    $out[] = $this->buildAttachmentsDocumentationPage($viewer, $engine);
+    $out[] = $this->buildPagingDocumentationPage($viewer, $engine);
 
     return $out;
   }
 
-  private function buildQueriesBox(
+  private function buildQueriesDocumentationPage(
+    PhabricatorUser $viewer,
     PhabricatorApplicationSearchEngine $engine) {
     $viewer = $this->getViewer();
 
@@ -140,15 +141,18 @@
           null,
         ));
 
-    return id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Builtin and Saved Queries'))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->newRemarkupDocumentationView($info))
-      ->appendChild($table);
+    $title = pht('Prebuilt Queries');
+    $content = array(
+      $this->newRemarkupDocumentationView($info),
+      $table,
+    );
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('queries');
   }
 
-  private function buildConstraintsBox(
+  private function buildConstraintsDocumentationPage(
+    PhabricatorUser $viewer,
     PhabricatorApplicationSearchEngine $engine) {
 
     $info = pht(<<<EOTEXT
@@ -281,16 +285,21 @@
           'wide',
         ));
 
-    return id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Custom Query Constraints'))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->newRemarkupDocumentationView($info))
-      ->appendChild($table)
-      ->appendChild($constant_lists);
+
+    $title = pht('Constraints');
+    $content = array(
+      $this->newRemarkupDocumentationView($info),
+      $table,
+      $constant_lists,
+    );
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('constraints')
+      ->setIconIcon('fa-filter');
   }
 
-  private function buildOrderBox(
+  private function buildOrderDocumentationPage(
+    PhabricatorUser $viewer,
     PhabricatorApplicationSearchEngine $engine,
     $query) {
 
@@ -388,18 +397,21 @@
           'wide',
         ));
 
-
-    return id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Result Ordering'))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->newRemarkupDocumentationView($orders_info))
-      ->appendChild($orders_table)
-      ->appendChild($this->newRemarkupDocumentationView($columns_info))
-      ->appendChild($columns_table);
+    $title = pht('Result Ordering');
+    $content = array(
+      $this->newRemarkupDocumentationView($orders_info),
+      $orders_table,
+      $this->newRemarkupDocumentationView($columns_info),
+      $columns_table,
+    );
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('ordering')
+      ->setIconIcon('fa-sort-numeric-asc');
   }
 
-  private function buildFieldsBox(
+  private function buildFieldsDocumentationPage(
+    PhabricatorUser $viewer,
     PhabricatorApplicationSearchEngine $engine) {
 
     $info = pht(<<<EOTEXT
@@ -470,15 +482,19 @@
           'wide',
         ));
 
-    return id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Object Fields'))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->newRemarkupDocumentationView($info))
-      ->appendChild($table);
+    $title = pht('Object Fields');
+    $content = array(
+      $this->newRemarkupDocumentationView($info),
+      $table,
+    );
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('fields')
+      ->setIconIcon('fa-cube');
   }
 
-  private function buildAttachmentsBox(
+  private function buildAttachmentsDocumentationPage(
+    PhabricatorUser $viewer,
     PhabricatorApplicationSearchEngine $engine) {
 
     $info = pht(<<<EOTEXT
@@ -560,15 +576,19 @@
           'wide',
         ));
 
-    return id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Attachments'))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->newRemarkupDocumentationView($info))
-      ->appendChild($table);
+    $title = pht('Attachments');
+    $content = array(
+      $this->newRemarkupDocumentationView($info),
+      $table,
+    );
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('attachments')
+      ->setIconIcon('fa-cubes');
   }
 
-  private function buildPagingBox(
+  private function buildPagingDocumentationPage(
+    PhabricatorUser $viewer,
     PhabricatorApplicationSearchEngine $engine) {
 
     $info = pht(<<<EOTEXT
@@ -631,11 +651,14 @@
 EOTEXT
       );
 
-    return id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Paging and Limits'))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->newRemarkupDocumentationView($info));
+    $title = pht('Paging and Limits');
+    $content = array(
+      $this->newRemarkupDocumentationView($info),
+    );
+
+    return $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('paging')
+      ->setIconIcon('fa-clone');
   }
 
 }
diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
--- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
+++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
@@ -13,7 +13,7 @@
       'or an entire object type.');
   }
 
-  public function getMethodDocumentation() {
+  protected function newDocumentationPages(PhabricatorUser $viewer) {
     $markup = pht(<<<EOREMARKUP
 When an object (like a task) is edited, Phabricator creates a "transaction"
 and applies it. This list of transactions on each object is the basis for
@@ -77,11 +77,10 @@
 
     $markup = $this->newRemarkupDocumentationView($markup);
 
-    return id(new PHUIObjectBoxView())
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->setHeaderText(pht('Method Details'))
-      ->appendChild($markup);
+    return array(
+      $this->newDocumentationBoxPage($viewer, pht('Method Details'), $markup)
+        ->setAnchor('details'),
+    );
   }
 
   protected function defineParamTypes() {
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php
--- a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php
@@ -38,9 +38,7 @@
       PhabricatorEnv::getDoclink('Conduit API: Using Edit Endpoints'));
   }
 
-  final public function getMethodDocumentation() {
-    $viewer = $this->getViewer();
-
+  final protected function newDocumentationPages(PhabricatorUser $viewer) {
     $engine = $this->newEditEngine()
       ->setViewer($viewer);
 
@@ -48,16 +46,15 @@
 
     $out = array();
 
-    $out[] = $this->buildEditTypesBoxes($engine, $types);
-
-    return $out;
+    return $this->buildEditTypesDocumentationPages($viewer, $engine, $types);
   }
 
-  private function buildEditTypesBoxes(
+  private function buildEditTypesDocumentationPages(
+    PhabricatorUser $viewer,
     PhabricatorEditEngine $engine,
     array $types) {
 
-    $boxes = array();
+    $pages = array();
 
     $summary_info = pht(
       'This endpoint supports these types of transactions. See below for '.
@@ -83,12 +80,14 @@
           'wide',
         ));
 
-    $boxes[] = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Transaction Types'))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->buildRemarkup($summary_info))
-      ->appendChild($summary_table);
+    $title = pht('Transaction Summary');
+    $content = array(
+      $this->buildRemarkup($summary_info),
+      $summary_table,
+    );
+
+    $pages[] = $this->newDocumentationBoxPage($viewer, $title, $content)
+      ->setAnchor('types');
 
     foreach ($types as $type) {
       $section = array();
@@ -130,15 +129,18 @@
             'wide',
           ));
 
-      $boxes[] = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Transaction Type: %s', $type->getEditType()))
-      ->setCollapsed(true)
-      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
-      ->appendChild($this->buildRemarkup($section))
-      ->appendChild($type_table);
+      $title = $type->getEditType();
+      $content = array(
+        $this->buildRemarkup($section),
+        $type_table,
+      );
+
+      $pages[] = $this->newDocumentationBoxPage($viewer, $title, $content)
+        ->setAnchor($type->getEditType())
+        ->setIconIcon('fa-pencil');
     }
 
-    return $boxes;
+    return $pages;
   }
 
 
diff --git a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
--- a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
+++ b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
@@ -21,7 +21,7 @@
   public function supportsObject(
     PhabricatorEditEngine $engine,
     PhabricatorApplicationTransactionInterface $object) {
-    return $engine->supportsSubtypes();
+    return ($object instanceof PhabricatorEditEngineSubtypeInterface);
   }
 
   public function buildCustomEditFields(
diff --git a/src/applications/uiexample/examples/PHUIBadgeExample.php b/src/applications/uiexample/examples/PHUIBadgeExample.php
--- a/src/applications/uiexample/examples/PHUIBadgeExample.php
+++ b/src/applications/uiexample/examples/PHUIBadgeExample.php
@@ -19,7 +19,7 @@
     $badges1 = array();
     $badges1[] = id(new PHUIBadgeView())
       ->setIcon('fa-users')
-      ->setHeader(pht('Phacility High Command'))
+      ->setHeader(pht('High Command'))
       ->setHref('/')
       ->setSource('Projects (automatic)')
       ->addByline(pht('Dec 31, 1969'))
@@ -113,7 +113,7 @@
       ->setHeader(pht('Lead Developer'))
       ->setSubhead(pht('Lead Developer of Phabricator'))
       ->setQuality(PhabricatorBadgesQuality::HEIRLOOM)
-      ->setSource(pht('Direct Award (epriestley)'))
+      ->setSource(pht('Direct Award'))
       ->addByline(pht('Dec 31, 1969'))
       ->addByline('1 Awarded (0.4%)');
 
diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php
--- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php
+++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php
@@ -322,6 +322,7 @@
     $default_user = PhabricatorEnv::getEnvConfig('mysql.user');
 
     $default_pass = PhabricatorEnv::getEnvConfig('mysql.pass');
+    $default_pass = phutil_string_cast($default_pass);
     $default_pass = new PhutilOpaqueEnvelope($default_pass);
 
     $config = PhabricatorEnv::getEnvConfig('cluster.databases');
diff --git a/src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php b/src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php
--- a/src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php
+++ b/src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php
@@ -11,9 +11,7 @@
     return pht('Read edge relationships between objects.');
   }
 
-  public function getMethodDocumentation() {
-    $viewer = $this->getViewer();
-
+  protected function newDocumentationPages(PhabricatorUser $viewer) {
     $rows = array();
     foreach ($this->getConduitEdgeTypeMap() as $key => $type) {
       $inverse_constant = $type->getInverseEdgeConstant();
@@ -48,17 +46,11 @@
           'wide',
         ));
 
-    return id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Edge Types'))
-      ->setTable($types_table);
-  }
-
-  public function getMethodStatus() {
-    return self::METHOD_STATUS_UNSTABLE;
-  }
 
-  public function getMethodStatusDescription() {
-    return pht('This method is new and experimental.');
+    return array(
+      $this->newDocumentationBoxPage($viewer, pht('Edge Types'), $types_table)
+        ->setAnchor('types'),
+    );
   }
 
   protected function defineParamTypes() {
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -42,7 +42,7 @@
   private $objects = array();
   private $viewer;
   private $contextObject;
-  private $version = 20;
+  private $version = 21;
   private $engineCaches = array();
   private $auxiliaryConfig = array();
 
@@ -504,6 +504,7 @@
 
     $rules = array();
     $rules[] = new PhutilRemarkupEscapeRemarkupRule();
+    $rules[] = new PhutilRemarkupEvalRule();
     $rules[] = new PhutilRemarkupMonospaceRule();
 
 
diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php
new file mode 100644
--- /dev/null
+++ b/src/infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php
@@ -0,0 +1,100 @@
+<?php
+
+final class PhutilRemarkupEvalRule extends PhutilRemarkupRule {
+
+  const KEY_EVAL = 'eval';
+
+  public function getPriority() {
+    return 50;
+  }
+
+  public function apply($text) {
+    return preg_replace_callback(
+      '/\${{{(.+?)}}}/',
+      array($this, 'newExpressionToken'),
+      $text);
+  }
+
+  public function newExpressionToken(array $matches) {
+    $expression = $matches[1];
+
+    if (!$this->isFlatText($expression)) {
+      return $matches[0];
+    }
+
+    $engine = $this->getEngine();
+    $token = $engine->storeText($expression);
+
+    $list_key = self::KEY_EVAL;
+    $expression_list = $engine->getTextMetadata($list_key, array());
+
+    $expression_list[] = array(
+      'token' => $token,
+      'expression' => $expression,
+      'original' => $matches[0],
+    );
+
+    $engine->setTextMetadata($list_key, $expression_list);
+
+    return $token;
+  }
+
+  public function didMarkupText() {
+    $engine = $this->getEngine();
+
+    $list_key = self::KEY_EVAL;
+    $expression_list = $engine->getTextMetadata($list_key, array());
+
+    foreach ($expression_list as $expression_item) {
+      $token = $expression_item['token'];
+      $expression = $expression_item['expression'];
+
+      $result = $this->evaluateExpression($expression);
+
+      if ($result === null) {
+        $result = $expression_item['original'];
+      }
+
+      $engine->overwriteStoredText($token, $result);
+    }
+  }
+
+  private function evaluateExpression($expression) {
+    static $string_map;
+
+    if ($string_map === null) {
+      $string_map = array(
+        'strings' => array(
+          'platform' => array(
+            'server' => array(
+              'name' => pht('Phabricator'),
+              'path' => pht('phabricator/'),
+            ),
+            'client' => array(
+              'name' => pht('Arcanist'),
+              'path' => pht('arcanist/'),
+            ),
+          ),
+        ),
+      );
+    }
+
+    $parts = explode('.', $expression);
+
+    $cursor = $string_map;
+    foreach ($parts as $part) {
+      if (isset($cursor[$part])) {
+        $cursor = $cursor[$part];
+      } else {
+        break;
+      }
+    }
+
+    if (is_string($cursor)) {
+      return $cursor;
+    }
+
+    return null;
+  }
+
+}
diff --git a/src/infrastructure/query/PhabricatorQuery.php b/src/infrastructure/query/PhabricatorQuery.php
--- a/src/infrastructure/query/PhabricatorQuery.php
+++ b/src/infrastructure/query/PhabricatorQuery.php
@@ -87,7 +87,7 @@
         foreach ($this->flattenSubclause($part) as $subpart) {
           $result[] = $subpart;
         }
-      } else if (strlen($part)) {
+      } else if (($part !== null) && strlen($part)) {
         $result[] = $part;
       }
     }
diff --git a/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php b/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
--- a/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
+++ b/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
@@ -57,6 +57,13 @@
       }
     }
 
+    // See T13588. In PHP 8.1, the default "report mode" for MySQLi has
+    // changed, which causes MySQLi to raise exceptions. Disable exceptions
+    // to align behavior with older default behavior under MySQLi, which
+    // this code expects. Plausibly, this code could be updated to use
+    // MySQLi exceptions to handle errors under a wider range of PHP versions.
+    mysqli_report(MYSQLI_REPORT_OFF);
+
     $conn = mysqli_init();
 
     $timeout = $this->getConfiguration('timeout');
diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php
--- a/src/infrastructure/storage/lisk/LiskDAO.php
+++ b/src/infrastructure/storage/lisk/LiskDAO.php
@@ -193,6 +193,8 @@
 
   private static $connections       = array();
 
+  private static $liskMetadata = array();
+
   protected $id;
   protected $phid;
   protected $dateCreated;
@@ -403,10 +405,11 @@
    *  @task   config
    */
   public function getConfigOption($option_name) {
-    static $options = null;
+    $options = $this->getLiskMetadata('config');
 
-    if (!isset($options)) {
+    if ($options === null) {
       $options = $this->getConfiguration();
+      $this->setLiskMetadata('config', $options);
     }
 
     return idx($options, $option_name);
@@ -439,7 +442,7 @@
 
     return $this->loadOneWhere(
       '%C = %d',
-      $this->getIDKeyForUse(),
+      $this->getIDKey(),
       $id);
   }
 
@@ -554,7 +557,7 @@
 
     $result = $this->loadOneWhere(
       '%C = %d',
-      $this->getIDKeyForUse(),
+      $this->getIDKey(),
       $this->getID());
 
     if (!$result) {
@@ -579,9 +582,10 @@
    * @task   load
    */
   public function loadFromArray(array $row) {
-    static $valid_properties = array();
+    $valid_map = $this->getLiskMetadata('validMap', array());
 
     $map = array();
+    $updated = false;
     foreach ($row as $k => $v) {
       // We permit (but ignore) extra properties in the array because a
       // common approach to building the array is to issue a raw SELECT query
@@ -594,14 +598,15 @@
       // path (assigning an invalid property which we've already seen) costs
       // an empty() plus an isset().
 
-      if (empty($valid_properties[$k])) {
-        if (isset($valid_properties[$k])) {
+      if (empty($valid_map[$k])) {
+        if (isset($valid_map[$k])) {
           // The value is set but empty, which means it's false, so we've
           // already determined it's not valid. We don't need to check again.
           continue;
         }
-        $valid_properties[$k] = $this->hasProperty($k);
-        if (!$valid_properties[$k]) {
+        $valid_map[$k] = $this->hasProperty($k);
+        $updated = true;
+        if (!$valid_map[$k]) {
           continue;
         }
       }
@@ -609,6 +614,10 @@
       $map[$k] = $v;
     }
 
+    if ($updated) {
+      $this->setLiskMetadata('validMap', $valid_map);
+    }
+
     $this->willReadData($map);
 
     foreach ($map as $prop => $value) {
@@ -686,10 +695,7 @@
    * @task   save
    */
   public function setID($id) {
-    static $id_key = null;
-    if ($id_key === null) {
-      $id_key = $this->getIDKeyForUse();
-    }
+    $id_key = $this->getIDKey();
     $this->$id_key = $id;
     return $this;
   }
@@ -704,10 +710,7 @@
    * @task   info
    */
   public function getID() {
-    static $id_key = null;
-    if ($id_key === null) {
-      $id_key = $this->getIDKeyForUse();
-    }
+    $id_key = $this->getIDKey();
     return $this->$id_key;
   }
 
@@ -742,9 +745,10 @@
    * @task   info
    */
   protected function getAllLiskProperties() {
-    static $properties = null;
-    if (!isset($properties)) {
-      $class = new ReflectionClass(get_class($this));
+    $properties = $this->getLiskMetadata('properties');
+
+    if ($properties === null) {
+      $class = new ReflectionClass(static::class);
       $properties = array();
       foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
         $properties[strtolower($p->getName())] = $p->getName();
@@ -763,7 +767,10 @@
       if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {
         unset($properties['phid']);
       }
+
+      $this->setLiskMetadata('properties', $properties);
     }
+
     return $properties;
   }
 
@@ -777,10 +784,7 @@
    * @task   info
    */
   protected function checkProperty($property) {
-    static $properties = null;
-    if ($properties === null) {
-      $properties = $this->getAllLiskProperties();
-    }
+    $properties = $this->getAllLiskProperties();
 
     $property = strtolower($property);
     if (empty($properties[$property])) {
@@ -996,7 +1000,7 @@
       'UPDATE %R SET %LQ WHERE %C = '.(is_int($id) ? '%d' : '%s'),
       $this,
       $map,
-      $this->getIDKeyForUse(),
+      $this->getIDKey(),
       $id);
     // We can't detect a missing object because updating an object without
     // changing any values doesn't affect rows. We could jiggle timestamps
@@ -1023,7 +1027,7 @@
     $conn->query(
       'DELETE FROM %R WHERE %C = %d',
       $this,
-      $this->getIDKeyForUse(),
+      $this->getIDKey(),
       $this->getID());
 
     $this->didDelete();
@@ -1051,7 +1055,7 @@
         // If we are using autoincrement IDs, let MySQL assign the value for the
         // ID column, if it is empty. If the caller has explicitly provided a
         // value, use it.
-        $id_key = $this->getIDKeyForUse();
+        $id_key = $this->getIDKey();
         if (empty($data[$id_key])) {
           unset($data[$id_key]);
         }
@@ -1059,7 +1063,7 @@
       case self::IDS_COUNTER:
         // If we are using counter IDs, assign a new ID if we don't already have
         // one.
-        $id_key = $this->getIDKeyForUse();
+        $id_key = $this->getIDKey();
         if (empty($data[$id_key])) {
           $counter_name = $this->getTableName();
           $id = self::loadNextCounterValue($conn, $counter_name);
@@ -1175,19 +1179,6 @@
     return 'id';
   }
 
-
-  protected function getIDKeyForUse() {
-    $id_key = $this->getIDKey();
-    if (!$id_key) {
-      throw new Exception(
-        pht(
-          'This DAO does not have a single-part primary key. The method you '.
-          'called requires a single-part primary key.'));
-    }
-    return $id_key;
-  }
-
-
   /**
    * Generate a new PHID, used by CONFIG_AUX_PHID.
    *
@@ -1592,22 +1583,12 @@
    * @task   util
    */
   public function __call($method, $args) {
-    // NOTE: PHP has a bug that static variables defined in __call() are shared
-    // across all children classes. Call a different method to work around this
-    // bug.
-    return $this->call($method, $args);
-  }
+    $dispatch_map = $this->getLiskMetadata('dispatchMap', array());
 
-  /**
-   * @task   util
-   */
-  final protected function call($method, $args) {
     // NOTE: This method is very performance-sensitive (many thousands of calls
     // per page on some pages), and thus has some silliness in the name of
     // optimizations.
 
-    static $dispatch_map = array();
-
     if ($method[0] === 'g') {
       if (isset($dispatch_map[$method])) {
         $property = $dispatch_map[$method];
@@ -1620,6 +1601,7 @@
           throw new Exception(pht('Bad getter call: %s', $method));
         }
         $dispatch_map[$method] = $property;
+        $this->setLiskMetadata('dispatchMap', $dispatch_map);
       }
 
       return $this->readField($property);
@@ -1632,12 +1614,14 @@
         if (substr($method, 0, 3) !== 'set') {
           throw new Exception(pht("Unable to resolve method '%s'!", $method));
         }
+
         $property = substr($method, 3);
         $property = $this->checkProperty($property);
         if (!$property) {
           throw new Exception(pht('Bad setter call: %s', $method));
         }
         $dispatch_map[$method] = $property;
+        $this->setLiskMetadata('dispatchMap', $dispatch_map);
       }
 
       $this->writeField($property, $args[0]);
@@ -1909,4 +1893,20 @@
   }
 
 
+  private function getLiskMetadata($key, $default = null) {
+    if (isset(self::$liskMetadata[static::class][$key])) {
+      return self::$liskMetadata[static::class][$key];
+    }
+
+    if (!isset(self::$liskMetadata[static::class])) {
+      self::$liskMetadata[static::class] = array();
+    }
+
+    return idx(self::$liskMetadata[static::class], $key, $default);
+  }
+
+  private function setLiskMetadata($key, $value) {
+    self::$liskMetadata[static::class][$key] = $value;
+  }
+
 }
diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
--- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
+++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
@@ -89,6 +89,17 @@
     return $this->namespace.'_'.$fragment;
   }
 
+  public function getInternalDatabaseName($name) {
+    $namespace = $this->getNamespace();
+
+    $prefix = $namespace.'_';
+    if (strncmp($name, $prefix, strlen($prefix))) {
+      return null;
+    }
+
+    return substr($name, strlen($prefix));
+  }
+
   public function getDisplayName() {
     return $this->getRef()->getDisplayName();
   }
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
@@ -44,6 +44,16 @@
               'With __--output__, overwrite the output file if it already '.
               'exists.'),
           ),
+          array(
+            'name' => 'database',
+            'param' => 'database-name',
+            'help' => pht(
+              'Dump only tables in the named database (or databases, if '.
+              'the flag is repeated). Specify database names without the '.
+              'namespace prefix (that is: use "differential", not '.
+              '"phabricator_differential").'),
+            'repeat' => true,
+          ),
         ));
   }
 
@@ -58,6 +68,8 @@
     $is_noindex = $args->getArg('no-indexes');
     $is_replica = $args->getArg('for-replica');
 
+    $database_filter = $args->getArg('database');
+
     if ($is_compress) {
       if ($output_file === null) {
         throw new PhutilArgumentUsageException(
@@ -128,11 +140,60 @@
     $schemata = $actual_map[$ref_key];
     $expect = $expect_map[$ref_key];
 
+    if ($database_filter) {
+      $internal_names = array();
+
+      $expect_databases = $expect->getDatabases();
+      foreach ($expect_databases as $expect_database) {
+        $database_name = $expect_database->getName();
+
+        $internal_name = $api->getInternalDatabaseName($database_name);
+        if ($internal_name !== null) {
+          $internal_names[$internal_name] = $database_name;
+        }
+      }
+
+      ksort($internal_names);
+
+      $seen = array();
+      foreach ($database_filter as $filter) {
+        if (!isset($internal_names[$filter])) {
+          throw new PhutilArgumentUsageException(
+            pht(
+              'Database "%s" is unknown. This script can only dump '.
+              'databases known to the current version of Phabricator. '.
+              'Valid databases are: %s.',
+              $filter,
+              implode(', ', array_keys($internal_names))));
+        }
+
+        if (isset($seen[$filter])) {
+          throw new PhutilArgumentUsageException(
+            pht(
+              'Database "%s" is specified more than once. Specify each '.
+              'database at most once.',
+              $filter));
+        }
+
+        $seen[$filter] = true;
+      }
+
+      $dump_databases = array_select_keys($internal_names, $database_filter);
+      $dump_databases = array_fuse($dump_databases);
+    } else {
+      $dump_databases = array_keys($schemata->getDatabases());
+      $dump_databases = array_fuse($dump_databases);
+    }
+
     $with_caches = $is_replica;
     $with_indexes = !$is_noindex;
 
     $targets = array();
     foreach ($schemata->getDatabases() as $database_name => $database) {
+      if (!isset($dump_databases[$database_name])) {
+        continue;
+      }
+
       $expect_database = $expect->getDatabase($database_name);
       foreach ($database->getTables() as $table_name => $table) {
 
diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php
--- a/src/infrastructure/util/PhabricatorHash.php
+++ b/src/infrastructure/util/PhabricatorHash.php
@@ -224,7 +224,7 @@
     $cache_key = "hmac.key({$hmac_name})";
 
     $hmac_key = $cache->getKey($cache_key);
-    if (!strlen($hmac_key)) {
+    if (($hmac_key === null) || !strlen($hmac_key)) {
       $hmac_key = self::readHMACKey($hmac_name);
 
       if ($hmac_key === null) {
diff --git a/src/infrastructure/util/PhabricatorMetronome.php b/src/infrastructure/util/PhabricatorMetronome.php
--- a/src/infrastructure/util/PhabricatorMetronome.php
+++ b/src/infrastructure/util/PhabricatorMetronome.php
@@ -49,7 +49,7 @@
   }
 
   public function setOffsetFromSeed($seed) {
-    $offset = PhabricatorHash::digestToRange($seed, 0, PHP_INT_MAX);
+    $offset = PhabricatorHash::digestToRange($seed, 0, 0x7FFFFFFF);
     return $this->setOffset($offset);
   }
 
diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php
--- a/support/startup/PhabricatorStartup.php
+++ b/support/startup/PhabricatorStartup.php
@@ -392,8 +392,11 @@
     ini_set('memory_limit', -1);
 
     // If we have libxml, disable the incredibly dangerous entity loader.
+    // PHP 8 deprecates this function and disables this by default; remove once
+    // PHP 7 is no longer supported or a future version has removed the function
+    // entirely.
     if (function_exists('libxml_disable_entity_loader')) {
-      libxml_disable_entity_loader(true);
+      @libxml_disable_entity_loader(true);
     }
 
     // See T13060. If the locale for this process (the parent process) is not
diff --git a/webroot/rsrc/css/phui/phui-list.css b/webroot/rsrc/css/phui/phui-list.css
--- a/webroot/rsrc/css/phui/phui-list.css
+++ b/webroot/rsrc/css/phui/phui-list.css
@@ -75,11 +75,10 @@
   padding: 4px 10px;
 }
 
-.phui-list-sidenav .phui-list-item-has-icon .phui-list-item-indented {
-  padding-left: 18px;
+.phabricator-side-menu .phui-list-item-has-icon .phui-list-item-indented {
+  padding-left: 24px;
 }
 
-
 .device-desktop .phui-list-sidenav .phui-list-item-href:hover {
   background: {$sky};
   color: white;
diff --git a/webroot/rsrc/externals/javelin/lib/DOM.js b/webroot/rsrc/externals/javelin/lib/DOM.js
--- a/webroot/rsrc/externals/javelin/lib/DOM.js
+++ b/webroot/rsrc/externals/javelin/lib/DOM.js
@@ -124,7 +124,7 @@
           'will not do the right thing with this.');
       }
 
-      // TODO(epriestley): May need to deny <option> more broadly, see
+      // TODO: May need to deny <option> more broadly, see
       // http://support.microsoft.com/kb/829907 and the whole mess in the
       // heavy stack. But I seem to have gotten away without cloning into the
       // documentFragment below, so this may be a nonissue.
@@ -147,7 +147,7 @@
       wrapper.innerHTML = this._content;
       var fragment = document.createDocumentFragment();
       while (wrapper.firstChild) {
-        // TODO(epriestley): Do we need to do a bunch of cloning junk here?
+        // TODO: Do we need to do a bunch of cloning junk here?
         // See heavy stack. I'm disconnecting the nodes instead; this seems
         // to work but maybe my test case just isn't extensive enough.
         fragment.appendChild(wrapper.removeChild(wrapper.firstChild));
diff --git a/webroot/rsrc/js/core/behavior-fancy-datepicker.js b/webroot/rsrc/js/core/behavior-fancy-datepicker.js
--- a/webroot/rsrc/js/core/behavior-fancy-datepicker.js
+++ b/webroot/rsrc/js/core/behavior-fancy-datepicker.js
@@ -425,6 +425,13 @@
             value_m += 12;
             value_y--;
           }
+          // This relies on months greater than 11 rolling over into the next
+          // year and days less than 1 rolling back into the previous month.
+          var last_date = new Date(value_y, value_m, 0);
+          if (value_d > last_date.getDate()) {
+            // The date falls outside the new month, so stuff it back in.
+            value_d = last_date.getDate();
+          }
           break;
         case 'd':
           // User clicked a day.