diff --git a/src/applications/differential/editor/DifferentialRevisionEditor.php b/src/applications/differential/editor/DifferentialRevisionEditor.php
index 028948d32..beefc3f11 100644
--- a/src/applications/differential/editor/DifferentialRevisionEditor.php
+++ b/src/applications/differential/editor/DifferentialRevisionEditor.php
@@ -1,1108 +1,1113 @@
 <?php
 
 /**
  * Handle major edit operations to DifferentialRevision -- adding and removing
  * reviewers, diffs, and CCs. Unlike simple edits, these changes trigger
  * complicated email workflows.
  */
 final class DifferentialRevisionEditor extends PhabricatorEditor {
 
   protected $revision;
 
   protected $cc         = null;
   protected $reviewers  = null;
   protected $diff;
   protected $comments;
   protected $silentUpdate;
 
   private $auxiliaryFields = array();
   private $contentSource;
   private $isCreate;
   private $aphrontRequestForEventDispatch;
 
 
   public function setAphrontRequestForEventDispatch(AphrontRequest $request) {
     $this->aphrontRequestForEventDispatch = $request;
     return $this;
   }
 
   public function getAphrontRequestForEventDispatch() {
     return $this->aphrontRequestForEventDispatch;
   }
 
   public function __construct(DifferentialRevision $revision) {
     $this->revision = $revision;
     $this->isCreate = !($revision->getID());
   }
 
   public static function newRevisionFromConduitWithDiff(
     array $fields,
     DifferentialDiff $diff,
     PhabricatorUser $actor) {
 
     $revision = DifferentialRevision::initializeNewRevision($actor);
     $revision->setPHID($revision->generatePHID());
 
     $editor = new DifferentialRevisionEditor($revision);
     $editor->setActor($actor);
     $editor->addDiff($diff, null);
     $editor->copyFieldsFromConduit($fields);
 
     $editor->save();
 
     return $revision;
   }
 
   public function copyFieldsFromConduit(array $fields) {
 
     $actor = $this->getActor();
     $revision = $this->revision;
     $revision->loadRelationships();
 
     $all_fields = DifferentialFieldSelector::newSelector()
       ->getFieldSpecifications();
 
     $aux_fields = array();
     foreach ($all_fields as $aux_field) {
       $aux_field->setRevision($revision);
       $aux_field->setDiff($this->diff);
       $aux_field->setUser($actor);
       if ($aux_field->shouldAppearOnCommitMessage()) {
         $aux_fields[$aux_field->getCommitMessageKey()] = $aux_field;
       }
     }
 
     foreach ($fields as $field => $value) {
       if (empty($aux_fields[$field])) {
         throw new Exception(
           "Parsed commit message contains unrecognized field '{$field}'.");
       }
       $aux_fields[$field]->setValueFromParsedCommitMessage($value);
     }
 
     foreach ($aux_fields as $aux_field) {
       $aux_field->validateField();
     }
 
     $this->setAuxiliaryFields($all_fields);
   }
 
   public function setAuxiliaryFields(array $auxiliary_fields) {
     assert_instances_of($auxiliary_fields, 'DifferentialFieldSpecification');
     $this->auxiliaryFields = $auxiliary_fields;
     return $this;
   }
 
   public function getRevision() {
     return $this->revision;
   }
 
   public function setReviewers(array $reviewers) {
     $this->reviewers = $reviewers;
     return $this;
   }
 
   public function setCCPHIDs(array $cc) {
     $this->cc = $cc;
     return $this;
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
 
   public function addDiff(DifferentialDiff $diff, $comments) {
     if ($diff->getRevisionID() &&
         $diff->getRevisionID() != $this->getRevision()->getID()) {
       $diff_id = (int)$diff->getID();
       $targ_id = (int)$this->getRevision()->getID();
       $real_id = (int)$diff->getRevisionID();
       throw new Exception(
         "Can not attach diff #{$diff_id} to Revision D{$targ_id}, it is ".
         "already attached to D{$real_id}.");
     }
     $this->diff = $diff;
     $this->comments = $comments;
 
     $repository = id(new DifferentialRepositoryLookup())
       ->setViewer($this->getActor())
       ->setDiff($diff)
       ->lookupRepository();
 
     if ($repository) {
       $this->getRevision()->setRepositoryPHID($repository->getPHID());
     }
 
     return $this;
   }
 
   protected function getDiff() {
     return $this->diff;
   }
 
   protected function getComments() {
     return $this->comments;
   }
 
   protected function getActorPHID() {
     return $this->getActor()->getPHID();
   }
 
   public function isNewRevision() {
     return !$this->getRevision()->getID();
   }
 
   /**
    * A silent update does not trigger Herald rules or send emails. This is used
    * for auto-amends at commit time.
    */
   public function setSilentUpdate($silent) {
     $this->silentUpdate = $silent;
     return $this;
   }
 
   public function save() {
     $revision = $this->getRevision();
 
     $is_new = $this->isNewRevision();
 
     $revision->loadRelationships();
 
     $this->willWriteRevision();
 
     if ($this->reviewers === null) {
       $this->reviewers = $revision->getReviewers();
     }
 
     if ($this->cc === null) {
       $this->cc = $revision->getCCPHIDs();
     }
 
     if ($is_new) {
       $content_blocks = array();
       foreach ($this->auxiliaryFields as $field) {
         if ($field->shouldExtractMentions()) {
           $content_blocks[] = $field->renderValueForCommitMessage(false);
         }
       }
       $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
         $content_blocks);
       $this->cc = array_unique(array_merge($this->cc, $phids));
     }
 
     $diff = $this->getDiff();
     if ($diff) {
       $revision->setLineCount($diff->getLineCount());
     }
 
     // Save the revision, to generate its ID and PHID if it is new. We need
     // the ID/PHID in order to record them in Herald transcripts, but don't
     // want to hold a transaction open while running Herald because it is
     // potentially somewhat slow. The downside is that we may end up with a
     // saved revision/diff pair without appropriate CCs. We could be better
     // about this -- for example:
     //
     //  - Herald can't affect reviewers, so we could compute them before
     //    opening the transaction and then save them in the transaction.
     //  - Herald doesn't *really* need PHIDs to compute its effects, we could
     //    run it before saving these objects and then hand over the PHIDs later.
     //
     // But this should address the problem of orphaned revisions, which is
     // currently the only problem we experience in practice.
 
     $revision->openTransaction();
 
       if ($diff) {
         $revision->setBranchName($diff->getBranch());
         $revision->setArcanistProjectPHID($diff->getArcanistProjectPHID());
       }
 
       $revision->save();
 
       if ($diff) {
         $diff->setRevisionID($revision->getID());
         $diff->save();
       }
 
     $revision->saveTransaction();
 
 
     // We're going to build up three dictionaries: $add, $rem, and $stable. The
     // $add dictionary has added reviewers/CCs. The $rem dictionary has
     // reviewers/CCs who have been removed, and the $stable array is
     // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
     // a different ("welcome") email than we send stable reviewers/CCs.
 
     $old = array(
       'rev' => array_fill_keys($revision->getReviewers(), true),
       'ccs' => array_fill_keys($revision->getCCPHIDs(), true),
     );
 
     $xscript_header = null;
     $xscript_uri = null;
 
     $new = array(
       'rev' => array_fill_keys($this->reviewers, true),
       'ccs' => array_fill_keys($this->cc, true),
     );
 
     $rem_ccs = array();
     $xscript_phid = null;
     if ($diff) {
       $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
         $revision,
         $diff);
       $adapter->setExplicitCCs($new['ccs']);
       $adapter->setExplicitReviewers($new['rev']);
       $adapter->setForbiddenCCs($revision->loadUnsubscribedPHIDs());
 
       $xscript = HeraldEngine::loadAndApplyRules($adapter);
       $xscript_uri = '/herald/transcript/'.$xscript->getID().'/';
       $xscript_phid = $xscript->getPHID();
       $xscript_header = $xscript->getXHeraldRulesHeader();
 
       $xscript_header = HeraldTranscript::saveXHeraldRulesHeader(
         $revision->getPHID(),
         $xscript_header);
 
       $sub = array(
         'rev' => $adapter->getReviewersAddedByHerald(),
         'ccs' => $adapter->getCCsAddedByHerald(),
       );
       $rem_ccs = $adapter->getCCsRemovedByHerald();
       $blocking_reviewers = array_keys(
         $adapter->getBlockingReviewersAddedByHerald());
+
+      HarbormasterBuildable::applyBuildPlans(
+        $diff->getPHID(),
+        $revision->getPHID(),
+        $adapter->getBuildPlans());
     } else {
       $sub = array(
         'rev' => array(),
         'ccs' => array(),
       );
       $blocking_reviewers = array();
     }
 
     // Remove any CCs which are prevented by Herald rules.
     $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
     $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
 
     $add = array();
     $rem = array();
     $stable = array();
     foreach (array('rev', 'ccs') as $key) {
       $add[$key] = array();
       if ($new[$key] !== null) {
         $add[$key] += array_diff_key($new[$key], $old[$key]);
       }
       $add[$key] += array_diff_key($sub[$key], $old[$key]);
 
       $combined = $sub[$key];
       if ($new[$key] !== null) {
         $combined += $new[$key];
       }
       $rem[$key] = array_diff_key($old[$key], $combined);
 
       $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
     }
 
     // Prevent Herald rules from adding a revision's owner as a reviewer.
     unset($add['rev'][$revision->getAuthorPHID()]);
 
     self::updateReviewers(
       $revision,
       $this->getActor(),
       array_keys($add['rev']),
       array_keys($rem['rev']),
       $blocking_reviewers);
 
     // We want to attribute new CCs to a "reasonPHID", representing the reason
     // they were added. This is either a user (if some user explicitly CCs
     // them, or uses "Add CCs...") or a Herald transcript PHID, indicating that
     // they were added by a Herald rule.
 
     if ($add['ccs'] || $rem['ccs']) {
       $reasons = array();
       foreach ($add['ccs'] as $phid => $ignored) {
         if (empty($new['ccs'][$phid])) {
           $reasons[$phid] = $xscript_phid;
         } else {
           $reasons[$phid] = $this->getActorPHID();
         }
       }
       foreach ($rem['ccs'] as $phid => $ignored) {
         if (empty($new['ccs'][$phid])) {
           $reasons[$phid] = $this->getActorPHID();
         } else {
           $reasons[$phid] = $xscript_phid;
         }
       }
     } else {
       $reasons = $this->getActorPHID();
     }
 
     self::alterCCs(
       $revision,
       $this->cc,
       array_keys($rem['ccs']),
       array_keys($add['ccs']),
       $reasons);
 
     $this->updateAuxiliaryFields();
 
     // Add the author and users included from Herald rules to the relevant set
     // of users so they get a copy of the email.
     if (!$this->silentUpdate) {
       if ($is_new) {
         $add['rev'][$this->getActorPHID()] = true;
         if ($diff) {
           $add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
         }
       } else {
         $stable['rev'][$this->getActorPHID()] = true;
         if ($diff) {
           $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
         }
       }
     }
 
     $mail = array();
 
     $phids = array($this->getActorPHID());
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($this->getActor())
       ->withPHIDs($phids)
       ->execute();
     $actor_handle = $handles[$this->getActorPHID()];
 
     $changesets = null;
     $comment = null;
     $old_status = $revision->getStatus();
 
     if ($diff) {
       $changesets = $diff->loadChangesets();
       // TODO: This should probably be in DifferentialFeedbackEditor?
       if (!$is_new) {
         $comment = $this->createComment();
       }
       if ($comment) {
         $mail[] = id(new DifferentialNewDiffMail(
             $revision,
             $actor_handle,
             $changesets))
           ->setActor($this->getActor())
           ->setIsFirstMailAboutRevision($is_new)
           ->setIsFirstMailToRecipients($is_new)
           ->setComments($this->getComments())
           ->setToPHIDs(array_keys($stable['rev']))
           ->setCCPHIDs(array_keys($stable['ccs']));
       }
 
       // Save the changes we made above.
 
       $diff->setDescription(preg_replace('/\n.*/s', '', $this->getComments()));
       $diff->save();
 
       $this->updateAffectedPathTable($revision, $diff, $changesets);
       $this->updateRevisionHashTable($revision, $diff);
 
       // An updated diff should require review, as long as it's not closed
       // or accepted. The "accepted" status is "sticky" to encourage courtesy
       // re-diffs after someone accepts with minor changes/suggestions.
 
       $status = $revision->getStatus();
       if ($status != ArcanistDifferentialRevisionStatus::CLOSED &&
           $status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
         $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
       }
 
     } else {
       $diff = $revision->loadActiveDiff();
       if ($diff) {
         $changesets = $diff->loadChangesets();
       } else {
         $changesets = array();
       }
     }
 
     $revision->save();
 
     // If the actor just deleted all the blocking/rejected reviewers, we may
     // be able to put the revision into "accepted".
     switch ($revision->getStatus()) {
       case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
       case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
         $revision = self::updateAcceptedStatus(
           $this->getActor(),
           $revision);
         break;
     }
 
     $this->didWriteRevision();
 
     $event_data = array(
       'revision_id'          => $revision->getID(),
       'revision_phid'        => $revision->getPHID(),
       'revision_name'        => $revision->getTitle(),
       'revision_author_phid' => $revision->getAuthorPHID(),
       'action'               => $is_new
         ? DifferentialAction::ACTION_CREATE
         : DifferentialAction::ACTION_UPDATE,
       'feedback_content'     => $is_new
         ? phutil_utf8_shorten($revision->getSummary(), 140)
         : $this->getComments(),
       'actor_phid'           => $revision->getAuthorPHID(),
     );
 
     $mailed_phids = array();
     if (!$this->silentUpdate) {
       $revision->loadRelationships();
 
       if ($add['rev']) {
         $message = id(new DifferentialNewDiffMail(
             $revision,
             $actor_handle,
             $changesets))
           ->setActor($this->getActor())
           ->setIsFirstMailAboutRevision($is_new)
           ->setIsFirstMailToRecipients(true)
           ->setToPHIDs(array_keys($add['rev']));
 
         if ($is_new) {
           // The first time we send an email about a revision, put the CCs in
           // the "CC:" field of the same "Review Requested" email that reviewers
           // get, so you don't get two initial emails if you're on a list that
           // is CC'd.
           $message->setCCPHIDs(array_keys($add['ccs']));
         }
 
         $mail[] = $message;
       }
 
       // If we added CCs, we want to send them an email, but only if they were
       // not already a reviewer and were not added as one (in these cases, they
       // got a "NewDiff" mail, either in the past or just a moment ago). You can
       // still get two emails, but only if a revision is updated and you are
       // added as a reviewer at the same time a list you are on is added as a
       // CC, which is rare and reasonable.
 
       $implied_ccs = self::getImpliedCCs($revision);
       $implied_ccs = array_fill_keys($implied_ccs, true);
       $add['ccs'] = array_diff_key($add['ccs'], $implied_ccs);
 
       if (!$is_new && $add['ccs']) {
         $mail[] = id(new DifferentialCCWelcomeMail(
             $revision,
             $actor_handle,
             $changesets))
           ->setActor($this->getActor())
           ->setIsFirstMailToRecipients(true)
           ->setToPHIDs(array_keys($add['ccs']));
       }
 
       foreach ($mail as $message) {
         $message->setHeraldTranscriptURI($xscript_uri);
         $message->setXHeraldRulesHeader($xscript_header);
         $message->send();
 
         $mailed_phids[] = $message->getRawMail()->buildRecipientList();
       }
       $mailed_phids = array_mergev($mailed_phids);
     }
 
     id(new PhabricatorFeedStoryPublisher())
       ->setStoryType('PhabricatorFeedStoryDifferential')
       ->setStoryData($event_data)
       ->setStoryTime(time())
       ->setStoryAuthorPHID($revision->getAuthorPHID())
       ->setRelatedPHIDs(
         array(
           $revision->getPHID(),
           $revision->getAuthorPHID(),
         ))
       ->setPrimaryObjectPHID($revision->getPHID())
       ->setSubscribedPHIDs(
         array_merge(
           array($revision->getAuthorPHID()),
           $revision->getReviewers(),
           $revision->getCCPHIDs()))
       ->setMailRecipientPHIDs($mailed_phids)
       ->publish();
 
     id(new PhabricatorSearchIndexer())
       ->indexDocumentByPHID($revision->getPHID());
   }
 
   public static function addCCAndUpdateRevision(
     $revision,
     $phid,
     PhabricatorUser $actor) {
 
     self::addCC($revision, $phid, $actor->getPHID());
 
     $type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER;
     id(new PhabricatorEdgeEditor())
       ->setActor($actor)
       ->removeEdge($revision->getPHID(), $type, $phid)
       ->save();
   }
 
   public static function removeCCAndUpdateRevision(
     $revision,
     $phid,
     PhabricatorUser $actor) {
 
     self::removeCC($revision, $phid, $actor->getPHID());
 
     $type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER;
     id(new PhabricatorEdgeEditor())
       ->setActor($actor)
       ->addEdge($revision->getPHID(), $type, $phid)
       ->save();
   }
 
   public static function addCC(
     DifferentialRevision $revision,
     $phid,
     $reason) {
     return self::alterCCs(
       $revision,
       $revision->getCCPHIDs(),
       $rem = array(),
       $add = array($phid),
       $reason);
   }
 
   public static function removeCC(
     DifferentialRevision $revision,
     $phid,
     $reason) {
     return self::alterCCs(
       $revision,
       $revision->getCCPHIDs(),
       $rem = array($phid),
       $add = array(),
       $reason);
   }
 
   protected static function alterCCs(
     DifferentialRevision $revision,
     array $stable_phids,
     array $rem_phids,
     array $add_phids,
     $reason_phid) {
 
     $dont_add = self::getImpliedCCs($revision);
     $add_phids = array_diff($add_phids, $dont_add);
 
     return self::alterRelationships(
       $revision,
       $stable_phids,
       $rem_phids,
       $add_phids,
       $reason_phid,
       DifferentialRevision::RELATION_SUBSCRIBED);
   }
 
   private static function getImpliedCCs(DifferentialRevision $revision) {
     return array_merge(
       $revision->getReviewers(),
       array($revision->getAuthorPHID()));
   }
 
   public static function updateReviewers(
     DifferentialRevision $revision,
     PhabricatorUser $actor,
     array $add_phids,
     array $remove_phids,
     array $blocking_phids = array()) {
 
     $reviewers = $revision->getReviewers();
 
     $editor = id(new PhabricatorEdgeEditor())
       ->setActor($actor);
 
     $reviewer_phids_map = array_fill_keys($reviewers, true);
 
     $blocking_phids = array_fuse($blocking_phids);
     foreach ($add_phids as $phid) {
 
       // Adding an already existing edge again would have cause memory loss
       // That is, the previous state for that reviewer would be lost
       if (isset($reviewer_phids_map[$phid])) {
         // TODO: If we're writing a blocking edge, we should overwrite an
         // existing weaker edge (like "added" or "commented"), just not a
         // stronger existing edge.
         continue;
       }
 
       if (isset($blocking_phids[$phid])) {
         $status = DifferentialReviewerStatus::STATUS_BLOCKING;
       } else {
         $status = DifferentialReviewerStatus::STATUS_ADDED;
       }
 
       $options = array(
         'data' => array(
           'status' => $status,
         )
       );
 
       $editor->addEdge(
         $revision->getPHID(),
         PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER,
         $phid,
         $options);
     }
 
     foreach ($remove_phids as $phid) {
       $editor->removeEdge(
         $revision->getPHID(),
         PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER,
         $phid);
     }
 
     $editor->save();
   }
 
   public static function updateReviewerStatus(
     DifferentialRevision $revision,
     PhabricatorUser $actor,
     $reviewer_phid,
     $status) {
 
     $options = array(
       'data' => array(
         'status' => $status
       )
     );
 
     $active_diff = $revision->loadActiveDiff();
     if ($active_diff) {
       $options['data']['diff'] = $active_diff->getID();
     }
 
     id(new PhabricatorEdgeEditor())
       ->setActor($actor)
       ->addEdge(
         $revision->getPHID(),
         PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER,
         $reviewer_phid,
         $options)
       ->save();
   }
 
   private static function alterRelationships(
     DifferentialRevision $revision,
     array $stable_phids,
     array $rem_phids,
     array $add_phids,
     $reason_phid,
     $relation_type) {
 
     $rem_map = array_fill_keys($rem_phids, true);
     $add_map = array_fill_keys($add_phids, true);
 
     $seq_map = array_values($stable_phids);
     $seq_map = array_flip($seq_map);
     foreach ($rem_map as $phid => $ignored) {
       if (!isset($seq_map[$phid])) {
         $seq_map[$phid] = count($seq_map);
       }
     }
     foreach ($add_map as $phid => $ignored) {
       if (!isset($seq_map[$phid])) {
         $seq_map[$phid] = count($seq_map);
       }
     }
 
     $raw = $revision->getRawRelations($relation_type);
     $raw = ipull($raw, null, 'objectPHID');
 
     $sequence = count($seq_map);
     foreach ($raw as $phid => $ignored) {
       if (isset($seq_map[$phid])) {
         $raw[$phid]['sequence'] = $seq_map[$phid];
       } else {
         $raw[$phid]['sequence'] = $sequence++;
       }
     }
     $raw = isort($raw, 'sequence');
 
     foreach ($raw as $phid => $ignored) {
       if (isset($rem_map[$phid])) {
         unset($raw[$phid]);
       }
     }
 
     foreach ($add_phids as $add) {
       $reason = is_array($reason_phid)
         ? idx($reason_phid, $add)
         : $reason_phid;
 
       $raw[$add] = array(
         'objectPHID'  => $add,
         'sequence'    => idx($seq_map, $add, $sequence++),
         'reasonPHID'  => $reason,
       );
     }
 
     $conn_w = $revision->establishConnection('w');
 
     $sql = array();
     foreach ($raw as $relation) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s, %s, %d, %s)',
         $revision->getID(),
         $relation_type,
         $relation['objectPHID'],
         $relation['sequence'],
         $relation['reasonPHID']);
     }
 
     $conn_w->openTransaction();
       queryfx(
         $conn_w,
         'DELETE FROM %T WHERE revisionID = %d AND relation = %s',
         DifferentialRevision::RELATIONSHIP_TABLE,
         $revision->getID(),
         $relation_type);
       if ($sql) {
         queryfx(
           $conn_w,
           'INSERT INTO %T
             (revisionID, relation, objectPHID, sequence, reasonPHID)
           VALUES %Q',
           DifferentialRevision::RELATIONSHIP_TABLE,
           implode(', ', $sql));
       }
     $conn_w->saveTransaction();
 
     $revision->loadRelationships();
   }
 
 
   private function createComment() {
     $comment = id(new DifferentialComment())
       ->setAuthorPHID($this->getActorPHID())
       ->setRevision($this->revision)
       ->setContent($this->getComments())
       ->setAction(DifferentialAction::ACTION_UPDATE)
       ->setMetadata(
         array(
           DifferentialComment::METADATA_DIFF_ID => $this->getDiff()->getID(),
         ));
 
     if ($this->contentSource) {
       $comment->setContentSource($this->contentSource);
     }
 
     $comment->save();
 
     return $comment;
   }
 
   private function updateAuxiliaryFields() {
     $aux_map = array();
     foreach ($this->auxiliaryFields as $aux_field) {
       $key = $aux_field->getStorageKey();
       if ($key !== null) {
         $val = $aux_field->getValueForStorage();
         $aux_map[$key] = $val;
       }
     }
 
     if (!$aux_map) {
       return;
     }
 
     $revision = $this->revision;
 
     $fields = id(new DifferentialAuxiliaryField())->loadAllWhere(
       'revisionPHID = %s AND name IN (%Ls)',
       $revision->getPHID(),
       array_keys($aux_map));
     $fields = mpull($fields, null, 'getName');
 
     foreach ($aux_map as $key => $val) {
       $obj = idx($fields, $key);
       if (!strlen($val)) {
         // If the new value is empty, just delete the old row if one exists and
         // don't add a new row if it doesn't.
         if ($obj) {
           $obj->delete();
         }
       } else {
         if (!$obj) {
           $obj = new DifferentialAuxiliaryField();
           $obj->setRevisionPHID($revision->getPHID());
           $obj->setName($key);
         }
 
         if ($obj->getValue() !== $val) {
           $obj->setValue($val);
           $obj->save();
         }
       }
     }
   }
 
   private function willWriteRevision() {
     foreach ($this->auxiliaryFields as $aux_field) {
       $aux_field->willWriteRevision($this);
     }
 
     $this->dispatchEvent(
       PhabricatorEventType::TYPE_DIFFERENTIAL_WILLEDITREVISION);
   }
 
   private function didWriteRevision() {
     foreach ($this->auxiliaryFields as $aux_field) {
       $aux_field->didWriteRevision($this);
     }
 
     $this->dispatchEvent(
       PhabricatorEventType::TYPE_DIFFERENTIAL_DIDEDITREVISION);
   }
 
   private function dispatchEvent($type) {
     $event = new PhabricatorEvent(
       $type,
       array(
         'revision'      => $this->revision,
         'new'           => $this->isCreate,
       ));
 
     $event->setUser($this->getActor());
 
     $request = $this->getAphrontRequestForEventDispatch();
     if ($request) {
       $event->setAphrontRequest($request);
     }
 
     PhutilEventEngine::dispatchEvent($event);
   }
 
   /**
    * Update the table which links Differential revisions to paths they affect,
    * so Diffusion can efficiently find pending revisions for a given file.
    */
   private function updateAffectedPathTable(
     DifferentialRevision $revision,
     DifferentialDiff $diff,
     array $changesets) {
     assert_instances_of($changesets, 'DifferentialChangeset');
 
     $project = $diff->loadArcanistProject();
     if (!$project) {
       // Probably an old revision from before projects.
       return;
     }
 
     $repository = $project->loadRepository();
     if (!$repository) {
       // Probably no project <-> repository link, or the repository where the
       // project lives is untracked.
       return;
     }
 
     $path_prefix = null;
 
     $local_root = $diff->getSourceControlPath();
     if ($local_root) {
       // We're in a working copy which supports subdirectory checkouts (e.g.,
       // SVN) so we need to figure out what prefix we should add to each path
       // (e.g., trunk/projects/example/) to get the absolute path from the
       // root of the repository. DVCS systems like Git and Mercurial are not
       // affected.
 
       // Normalize both paths and check if the repository root is a prefix of
       // the local root. If so, throw it away. Note that this correctly handles
       // the case where the remote path is "/".
       $local_root = id(new PhutilURI($local_root))->getPath();
       $local_root = rtrim($local_root, '/');
 
       $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
       $repo_root = rtrim($repo_root, '/');
 
       if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
         $path_prefix = substr($local_root, strlen($repo_root));
       }
     }
 
     $paths = array();
     foreach ($changesets as $changeset) {
       $paths[] = $path_prefix.'/'.$changeset->getFilename();
     }
 
     // Mark this as also touching all parent paths, so you can see all pending
     // changes to any file within a directory.
     $all_paths = array();
     foreach ($paths as $local) {
       foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
         $all_paths[$path] = true;
       }
     }
     $all_paths = array_keys($all_paths);
 
     $path_ids =
       PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
         $all_paths);
 
     $table = new DifferentialAffectedPath();
     $conn_w = $table->establishConnection('w');
 
     $sql = array();
     foreach ($path_ids as $path_id) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %d, %d, %d)',
         $repository->getID(),
         $path_id,
         time(),
         $revision->getID());
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE revisionID = %d',
       $table->getTableName(),
       $revision->getID());
     foreach (array_chunk($sql, 256) as $chunk) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
         $table->getTableName(),
         implode(', ', $chunk));
     }
   }
 
 
   /**
    * Update the table connecting revisions to DVCS local hashes, so we can
    * identify revisions by commit/tree hashes.
    */
   private function updateRevisionHashTable(
     DifferentialRevision $revision,
     DifferentialDiff $diff) {
 
     $vcs = $diff->getSourceControlSystem();
     if ($vcs == DifferentialRevisionControlSystem::SVN) {
       // Subversion has no local commit or tree hash information, so we don't
       // have to do anything.
       return;
     }
 
     $property = id(new DifferentialDiffProperty())->loadOneWhere(
       'diffID = %d AND name = %s',
       $diff->getID(),
       'local:commits');
     if (!$property) {
       return;
     }
 
     $hashes = array();
 
     $data = $property->getData();
     switch ($vcs) {
       case DifferentialRevisionControlSystem::GIT:
         foreach ($data as $commit) {
           $hashes[] = array(
             ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
             $commit['commit'],
           );
           $hashes[] = array(
             ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
             $commit['tree'],
           );
         }
         break;
       case DifferentialRevisionControlSystem::MERCURIAL:
         foreach ($data as $commit) {
           $hashes[] = array(
             ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
             $commit['rev'],
           );
         }
         break;
     }
 
     $conn_w = $revision->establishConnection('w');
 
     $sql = array();
     foreach ($hashes as $info) {
       list($type, $hash) = $info;
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s, %s)',
         $revision->getID(),
         $type,
         $hash);
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE revisionID = %d',
       ArcanistDifferentialRevisionHash::TABLE_NAME,
       $revision->getID());
 
     if ($sql) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
         ArcanistDifferentialRevisionHash::TABLE_NAME,
         implode(', ', $sql));
     }
   }
 
   /**
    * Try to move a revision to "accepted". We look for:
    *
    *   - at least one accepting reviewer who is a user; and
    *   - no rejects; and
    *   - no blocking reviewers.
    */
   public static function updateAcceptedStatus(
     PhabricatorUser $viewer,
     DifferentialRevision $revision) {
 
     $revision = id(new DifferentialRevisionQuery())
       ->setViewer($viewer)
       ->withIDs(array($revision->getID()))
       ->needRelationships(true)
       ->needReviewerStatus(true)
       ->needReviewerAuthority(true)
       ->executeOne();
 
     $has_user_accept = false;
     foreach ($revision->getReviewerStatus() as $reviewer) {
       $status = $reviewer->getStatus();
       if ($status == DifferentialReviewerStatus::STATUS_BLOCKING) {
         // We have a blocking reviewer, so just leave the revision in its
         // existing state.
         return $revision;
       }
 
       if ($status == DifferentialReviewerStatus::STATUS_REJECTED) {
         // We have a rejecting reviewer, so leave the revisoin as is.
         return $revision;
       }
 
       if ($reviewer->isUser()) {
         if ($status == DifferentialReviewerStatus::STATUS_ACCEPTED) {
           $has_user_accept = true;
         }
       }
     }
 
     if ($has_user_accept) {
       $revision
         ->setStatus(ArcanistDifferentialRevisionStatus::ACCEPTED)
         ->save();
     }
 
     return $revision;
   }
 
 }
 
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableApplyController.php b/src/applications/harbormaster/controller/HarbormasterBuildableApplyController.php
index 215e11795..b1eff91f1 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildableApplyController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableApplyController.php
@@ -1,81 +1,74 @@
 <?php
 
 final class HarbormasterBuildableApplyController
   extends HarbormasterController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $id = $this->id;
 
     $buildable = id(new HarbormasterBuildableQuery())
       ->setViewer($viewer)
       ->withIDs(array($id))
       ->executeOne();
     if ($buildable === null) {
       throw new Exception("Buildable not found!");
     }
 
     $buildable_uri = '/B'.$buildable->getID();
 
     if ($request->isDialogFormPost()) {
       $plan = id(new HarbormasterBuildPlanQuery())
         ->setViewer($viewer)
         ->withIDs(array($request->getInt('build-plan')))
         ->executeOne();
 
-      $build = HarbormasterBuild::initializeNewBuild($viewer);
-      $build->setBuildablePHID($buildable->getPHID());
-      $build->setBuildPlanPHID($plan->getPHID());
-      $build->setBuildStatus(HarbormasterBuild::STATUS_PENDING);
-      $build->save();
-
-      PhabricatorWorker::scheduleTask(
-        'HarbormasterBuildWorker',
-        array(
-          'buildID' => $build->getID()
-        ));
+      HarbormasterBuildable::applyBuildPlans(
+        $buildable->getBuildablePHID(),
+        $buildable->getContainerPHID(),
+        array($plan->getPHID()));
 
       return id(new AphrontRedirectResponse())->setURI($buildable_uri);
     }
 
     $plans = id(new HarbormasterBuildPlanQuery())
       ->setViewer($viewer)
       ->execute();
 
     $options = array();
     foreach ($plans as $plan) {
       $options[$plan->getID()] = $plan->getName();
     }
 
     // FIXME: I'd really like to use the dialog that "Edit Differential
     // Revisions" uses, but that code is quite hard-coded for the particular
     // uses, so for now we just give a single dropdown.
 
     $dialog = new AphrontDialogView();
     $dialog->setTitle(pht('Apply which plan?'))
       ->setUser($viewer)
       ->addSubmitButton(pht('Apply'))
       ->addCancelButton($buildable_uri);
     $dialog->appendChild(
       phutil_tag(
         'p',
         array(),
         pht(
           'Select what build plan you want to apply to this buildable:')));
     $dialog->appendChild(
       id(new AphrontFormSelectControl())
         ->setUser($viewer)
         ->setName('build-plan')
         ->setOptions($options));
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
 }
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
index a5c9c0b34..c1eb56496 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
@@ -1,144 +1,150 @@
 <?php
 
 final class HarbormasterBuildableViewController
   extends HarbormasterController {
 
   private $id;
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $id = $this->id;
 
     $buildable = id(new HarbormasterBuildableQuery())
       ->setViewer($viewer)
       ->withIDs(array($id))
       ->needBuildableHandles(true)
-      ->needContainerObjects(true)
+      ->needContainerHandles(true)
       ->executeOne();
     if (!$buildable) {
       return new Aphront404Response();
     }
 
     $builds = id(new HarbormasterBuildQuery())
       ->setViewer($viewer)
       ->withBuildablePHIDs(array($buildable->getPHID()))
       ->needBuildPlans(true)
       ->execute();
 
     $build_list = id(new PHUIObjectItemListView())
       ->setUser($viewer);
     foreach ($builds as $build) {
       $item = id(new PHUIObjectItemView())
         ->setObjectName(pht('Build %d', $build->getID()))
         ->setHeader($build->getName());
       switch ($build->getBuildStatus()) {
         case HarbormasterBuild::STATUS_INACTIVE:
           $item->setBarColor('grey');
           $item->addAttribute(pht('Inactive'));
           break;
         case HarbormasterBuild::STATUS_PENDING:
           $item->setBarColor('blue');
           $item->addAttribute(pht('Pending'));
           break;
         case HarbormasterBuild::STATUS_WAITING:
           $item->setBarColor('blue');
           $item->addAttribute(pht('Waiting on Resource'));
           break;
         case HarbormasterBuild::STATUS_BUILDING:
           $item->setBarColor('yellow');
           $item->addAttribute(pht('Building'));
           break;
         case HarbormasterBuild::STATUS_PASSED:
           $item->setBarColor('green');
           $item->addAttribute(pht('Passed'));
           break;
         case HarbormasterBuild::STATUS_FAILED:
           $item->setBarColor('red');
           $item->addAttribute(pht('Failed'));
           break;
         case HarbormasterBuild::STATUS_ERROR:
           $item->setBarColor('red');
           $item->addAttribute(pht('Unexpected Error'));
           break;
       }
       $build_list->addItem($item);
     }
 
     $title = pht("Buildable %d", $id);
 
     $header = id(new PHUIHeaderView())
       ->setHeader($title)
       ->setUser($viewer)
       ->setPolicyObject($buildable);
 
     $box = id(new PHUIObjectBoxView())
       ->setHeader($header);
 
     $actions = $this->buildActionList($buildable);
     $this->buildPropertyLists($box, $buildable, $actions);
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addCrumb(
       id(new PhabricatorCrumbView())
         ->setName("B{$id}"));
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $box,
         $build_list,
       ),
       array(
         'title' => $title,
         'device' => true,
       ));
   }
 
   private function buildActionList(HarbormasterBuildable $buildable) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
     $id = $buildable->getID();
 
     $list = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObject($buildable)
       ->setObjectURI("/B{$id}");
 
     $apply_uri = $this->getApplicationURI('/buildable/apply/'.$id.'/');
 
     $list->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Apply Build Plan'))
         ->setIcon('edit')
         ->setHref($apply_uri)
         ->setWorkflow(true));
 
     return $list;
   }
 
   private function buildPropertyLists(
     PHUIObjectBoxView $box,
     HarbormasterBuildable $buildable,
     PhabricatorActionListView $actions) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $properties = id(new PHUIPropertyListView())
       ->setUser($viewer)
       ->setObject($buildable)
       ->setActionList($actions);
     $box->addPropertyList($properties);
 
     $properties->addProperty(
       pht('Buildable'),
       $buildable->getBuildableHandle()->renderLink());
 
+    if ($buildable->getContainerHandle() !== null) {
+      $properties->addProperty(
+        pht('Container'),
+        $buildable->getContainerHandle()->renderLink());
+    }
+
   }
 
 }
diff --git a/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php
index c378902f5..0a18c73d8 100644
--- a/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php
+++ b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php
@@ -1,37 +1,39 @@
 <?php
 
 final class HarbormasterPHIDTypeBuildPlan extends PhabricatorPHIDType {
 
   const TYPECONST = 'HMCP';
 
   public function getTypeConstant() {
     return self::TYPECONST;
   }
 
   public function getTypeName() {
     return pht('Build Plan');
   }
 
   public function newObject() {
     return new HarbormasterBuildPlan();
   }
 
   protected function buildQueryForObjects(
     PhabricatorObjectQuery $query,
     array $phids) {
 
     return id(new HarbormasterBuildPlanQuery())
       ->withPHIDs($phids);
   }
 
   public function loadHandles(
     PhabricatorHandleQuery $query,
     array $handles,
     array $objects) {
 
     foreach ($handles as $phid => $handle) {
       $build_plan = $objects[$phid];
+      $handles[$phid]->setName($build_plan->getName());
+      $handles[$phid]->setURI('/harbormaster/plan/'.$build_plan->getID());
     }
   }
 
 }
diff --git a/src/applications/harbormaster/query/HarbormasterBuildableQuery.php b/src/applications/harbormaster/query/HarbormasterBuildableQuery.php
index decf9d8a4..1aae1b424 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildableQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildableQuery.php
@@ -1,183 +1,209 @@
 <?php
 
 final class HarbormasterBuildableQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $phids;
   private $buildablePHIDs;
   private $containerPHIDs;
 
   private $needContainerObjects;
+  private $needContainerHandles;
   private $needBuildableHandles;
   private $needBuilds;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withBuildablePHIDs(array $buildable_phids) {
     $this->buildablePHIDs = $buildable_phids;
     return $this;
   }
 
   public function withContainerPHIDs(array $container_phids) {
     $this->containerPHIDs = $container_phids;
     return $this;
   }
 
   public function needContainerObjects($need) {
     $this->needContainerObjects = $need;
     return $this;
   }
 
+  public function needContainerHandles($need) {
+    $this->needContainerHandles = $need;
+    return $this;
+  }
+
   public function needBuildableHandles($need) {
     $this->needBuildableHandles = $need;
     return $this;
   }
 
   public function needBuilds($need) {
     $this->needBuilds = $need;
     return $this;
   }
 
   protected function loadPage() {
     $table = new HarbormasterBuildable();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     return $table->loadAllFromArray($data);
   }
 
   protected function willFilterPage(array $page) {
     $buildables = array();
 
     $buildable_phids = array_filter(mpull($page, 'getBuildablePHID'));
     if ($buildable_phids) {
       $buildables = id(new PhabricatorObjectQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs($buildable_phids)
         ->setParentQuery($this)
         ->execute();
       $buildables = mpull($buildables, null, 'getPHID');
     }
 
     foreach ($page as $key => $buildable) {
       $buildable_phid = $buildable->getBuildablePHID();
       if (empty($buildables[$buildable_phid])) {
         unset($page[$key]);
         continue;
       }
       $buildable->attachBuildableObject($buildables[$buildable_phid]);
     }
 
     return $page;
   }
 
   protected function didFilterPage(array $page) {
-    if ($this->needContainerObjects) {
-      $containers = array();
-
+    if ($this->needContainerObjects || $this->needContainerHandles) {
       $container_phids = array_filter(mpull($page, 'getContainerPHID'));
-      if ($container_phids) {
-        $containers = id(new PhabricatorObjectQuery())
-          ->setViewer($this->getViewer())
-          ->withPHIDs($container_phids)
-          ->setParentQuery($this)
-          ->execute();
-        $containers = mpull($containers, null, 'getPHID');
+
+      if ($this->needContainerObjects) {
+        $containers = array();
+
+        if ($container_phids) {
+          $containers = id(new PhabricatorObjectQuery())
+            ->setViewer($this->getViewer())
+            ->withPHIDs($container_phids)
+            ->setParentQuery($this)
+            ->execute();
+          $containers = mpull($containers, null, 'getPHID');
+        }
+
+        foreach ($page as $key => $buildable) {
+          $container_phid = $buildable->getContainerPHID();
+          $buildable->attachContainerObject(idx($containers, $container_phid));
+        }
       }
 
-      foreach ($page as $key => $buildable) {
-        $container_phid = $buildable->getContainerPHID();
-        $buildable->attachContainerObject(idx($containers, $container_phid));
+      if ($this->needContainerHandles) {
+        $handles = array();
+
+        if ($container_phids) {
+          $handles = id(new PhabricatorHandleQuery())
+            ->setViewer($this->getViewer())
+            ->withPHIDs($container_phids)
+            ->setParentQuery($this)
+            ->execute();
+        }
+
+        foreach ($page as $key => $buildable) {
+          $container_phid = $buildable->getContainerPHID();
+          $buildable->attachContainerHandle(idx($handles, $container_phid));
+        }
       }
     }
 
     if ($this->needBuildableHandles) {
       $handles = array();
 
       $handle_phids = array_filter(mpull($page, 'getBuildablePHID'));
       if ($handle_phids) {
         $handles = id(new PhabricatorHandleQuery())
           ->setViewer($this->getViewer())
           ->withPHIDs($handle_phids)
           ->setParentQuery($this)
           ->execute();
       }
 
       foreach ($page as $key => $buildable) {
         $handle_phid = $buildable->getBuildablePHID();
         $buildable->attachBuildableHandle(idx($handles, $handle_phid));
       }
     }
 
     if ($this->needBuilds) {
       $builds = id(new HarbormasterBuildQuery())
         ->setViewer($this->getViewer())
         ->setParentQuery($this)
         ->withBuildablePHIDs(mpull($page, 'getPHID'))
         ->execute();
       $builds = mgroup($builds, 'getBuildablePHID');
       foreach ($page as $key => $buildable) {
         $buildable->attachBuilds(idx($builds, $buildable->getPHID(), array()));
       }
     }
 
     return $page;
   }
 
   private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
     $where = array();
 
     if ($this->ids) {
       $where[] = qsprintf(
         $conn_r,
         'id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids) {
       $where[] = qsprintf(
         $conn_r,
         'phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->buildablePHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'buildablePHID IN (%Ls)',
         $this->buildablePHIDs);
     }
 
     if ($this->containerPHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'containerPHID in (%Ls)',
         $this->containerPHIDs);
     }
 
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorApplicationHarbormaster';
   }
 
 }
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php
index ebaf15842..0dcb9e86c 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildable.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php
@@ -1,98 +1,177 @@
 <?php
 
 final class HarbormasterBuildable extends HarbormasterDAO
   implements PhabricatorPolicyInterface {
 
   protected $buildablePHID;
   protected $containerPHID;
   protected $buildStatus;
   protected $buildableStatus;
 
   private $buildableObject = self::ATTACHABLE;
   private $containerObject = self::ATTACHABLE;
   private $buildableHandle = self::ATTACHABLE;
+  private $containerHandle = self::ATTACHABLE;
   private $builds = self::ATTACHABLE;
 
   const STATUS_WHATEVER = 'whatever';
 
   public static function initializeNewBuildable(PhabricatorUser $actor) {
     return id(new HarbormasterBuildable())
       ->setBuildStatus(self::STATUS_WHATEVER)
       ->setBuildableStatus(self::STATUS_WHATEVER);
   }
 
+  /**
+   * Returns an existing buildable for the object's PHID or creates a
+   * new buildable implicitly if needed.
+   */
+  public static function createOrLoadExisting(
+    PhabricatorUser $actor,
+    $buildable_object_phid,
+    $container_object_phid) {
+
+    $buildable = id(new HarbormasterBuildableQuery())
+      ->setViewer($actor)
+      ->withBuildablePHIDs(array($buildable_object_phid))
+      ->executeOne();
+    if ($buildable) {
+      return $buildable;
+    }
+    $buildable = HarbormasterBuildable::initializeNewBuildable($actor)
+      ->setBuildablePHID($buildable_object_phid)
+      ->setContainerPHID($container_object_phid);
+    $buildable->save();
+    return $buildable;
+  }
+
+  /**
+   * Looks up the plan PHIDs and applies the plans to the specified
+   * object identified by it's PHID.
+   */
+  public static function applyBuildPlans(
+    $phid,
+    $container_phid,
+    array $plan_phids) {
+
+    if (count($plan_phids) === 0) {
+      return;
+    }
+
+    // Skip all of this logic if the Harbormaster application
+    // isn't currently installed.
+
+    $harbormaster_app = 'PhabricatorApplicationHarbormaster';
+    if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) {
+      return;
+    }
+
+    $buildable = HarbormasterBuildable::createOrLoadExisting(
+      PhabricatorUser::getOmnipotentUser(),
+      $phid,
+      $container_phid);
+
+    $plans = id(new HarbormasterBuildPlanQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withPHIDs($plan_phids)
+      ->execute();
+    foreach ($plans as $plan) {
+      $build = HarbormasterBuild::initializeNewBuild(
+        PhabricatorUser::getOmnipotentUser());
+      $build->setBuildablePHID($buildable->getPHID());
+      $build->setBuildPlanPHID($plan->getPHID());
+      $build->setBuildStatus(HarbormasterBuild::STATUS_PENDING);
+      $build->save();
+
+      PhabricatorWorker::scheduleTask(
+        'HarbormasterBuildWorker',
+        array(
+          'buildID' => $build->getID()
+        ));
+    }
+  }
+
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       HarbormasterPHIDTypeBuildable::TYPECONST);
   }
 
   public function attachBuildableObject($buildable_object) {
     $this->buildableObject = $buildable_object;
     return $this;
   }
 
   public function getBuildableObject() {
     return $this->assertAttached($this->buildableObject);
   }
 
   public function attachContainerObject($container_object) {
     $this->containerObject = $container_object;
     return $this;
   }
 
   public function getContainerObject() {
     return $this->assertAttached($this->containerObject);
   }
 
+  public function attachContainerHandle($container_handle) {
+    $this->containerHandle = $container_handle;
+    return $this;
+  }
+
+  public function getContainerHandle() {
+    return $this->assertAttached($this->containerHandle);
+  }
+
   public function attachBuildableHandle($buildable_handle) {
     $this->buildableHandle = $buildable_handle;
     return $this;
   }
 
   public function getBuildableHandle() {
     return $this->assertAttached($this->buildableHandle);
   }
 
   public function attachBuilds(array $builds) {
     assert_instances_of($builds, 'HarbormasterBuild');
     $this->builds = $builds;
     return $this;
   }
 
   public function getBuilds() {
     return $this->assertAttached($this->builds);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getBuildableObject()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getBuildableObject()->hasAutomaticCapability(
       $capability,
       $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht(
       'Users must be able to see the revision or repository to see a '.
       'buildable.');
   }
 
 }
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index 041e01ade..40cd9c8aa 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,900 +1,905 @@
 <?php
 
 /**
  * @group herald
  */
 abstract class HeraldAdapter {
 
   const FIELD_TITLE                  = 'title';
   const FIELD_BODY                   = 'body';
   const FIELD_AUTHOR                 = 'author';
   const FIELD_REVIEWER               = 'reviewer';
   const FIELD_REVIEWERS              = 'reviewers';
   const FIELD_COMMITTER              = 'committer';
   const FIELD_CC                     = 'cc';
   const FIELD_TAGS                   = 'tags';
   const FIELD_DIFF_FILE              = 'diff-file';
   const FIELD_DIFF_CONTENT           = 'diff-content';
   const FIELD_DIFF_ADDED_CONTENT     = 'diff-added-content';
   const FIELD_DIFF_REMOVED_CONTENT   = 'diff-removed-content';
   const FIELD_REPOSITORY             = 'repository';
   const FIELD_RULE                   = 'rule';
   const FIELD_AFFECTED_PACKAGE       = 'affected-package';
   const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
   const FIELD_CONTENT_SOURCE         = 'contentsource';
   const FIELD_ALWAYS                 = 'always';
   const FIELD_AUTHOR_PROJECTS        = 'authorprojects';
 
   const CONDITION_CONTAINS        = 'contains';
   const CONDITION_NOT_CONTAINS    = '!contains';
   const CONDITION_IS              = 'is';
   const CONDITION_IS_NOT          = '!is';
   const CONDITION_IS_ANY          = 'isany';
   const CONDITION_IS_NOT_ANY      = '!isany';
   const CONDITION_INCLUDE_ALL     = 'all';
   const CONDITION_INCLUDE_ANY     = 'any';
   const CONDITION_INCLUDE_NONE    = 'none';
   const CONDITION_IS_ME           = 'me';
   const CONDITION_IS_NOT_ME       = '!me';
   const CONDITION_REGEXP          = 'regexp';
   const CONDITION_RULE            = 'conditions';
   const CONDITION_NOT_RULE        = '!conditions';
   const CONDITION_EXISTS          = 'exists';
   const CONDITION_NOT_EXISTS      = '!exists';
   const CONDITION_UNCONDITIONALLY = 'unconditionally';
   const CONDITION_REGEXP_PAIR     = 'regexp-pair';
 
   const ACTION_ADD_CC       = 'addcc';
   const ACTION_REMOVE_CC    = 'remcc';
   const ACTION_EMAIL        = 'email';
   const ACTION_NOTHING      = 'nothing';
   const ACTION_AUDIT        = 'audit';
   const ACTION_FLAG         = 'flag';
   const ACTION_ASSIGN_TASK  = 'assigntask';
   const ACTION_ADD_PROJECTS = 'addprojects';
   const ACTION_ADD_REVIEWERS = 'addreviewers';
   const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
+  const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
 
   const VALUE_TEXT            = 'text';
   const VALUE_NONE            = 'none';
   const VALUE_EMAIL           = 'email';
   const VALUE_USER            = 'user';
   const VALUE_TAG             = 'tag';
   const VALUE_RULE            = 'rule';
   const VALUE_REPOSITORY      = 'repository';
   const VALUE_OWNERS_PACKAGE  = 'package';
   const VALUE_PROJECT         = 'project';
   const VALUE_FLAG_COLOR      = 'flagcolor';
   const VALUE_CONTENT_SOURCE  = 'contentsource';
   const VALUE_USER_OR_PROJECT = 'userorproject';
+  const VALUE_BUILD_PLAN      = 'buildplan';
 
   private $contentSource;
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
   public function getContentSource() {
     return $this->contentSource;
   }
 
   abstract public function getPHID();
   abstract public function getHeraldName();
 
   public function getHeraldField($field_name) {
     switch ($field_name) {
       case self::FIELD_RULE:
         return null;
       case self::FIELD_CONTENT_SOURCE:
         return $this->getContentSource()->getSource();
       case self::FIELD_ALWAYS:
         return true;
       default:
         throw new Exception(
           "Unknown field '{$field_name}'!");
     }
   }
 
   abstract public function applyHeraldEffects(array $effects);
 
   public function isAvailableToUser(PhabricatorUser $viewer) {
     $applications = id(new PhabricatorApplicationQuery())
       ->setViewer($viewer)
       ->withInstalled(true)
       ->withClasses(array($this->getAdapterApplicationClass()))
       ->execute();
 
     return !empty($applications);
   }
 
 
   /**
    * NOTE: You generally should not override this; it exists to support legacy
    * adapters which had hard-coded content types.
    */
   public function getAdapterContentType() {
     return get_class($this);
   }
 
   abstract public function getAdapterContentName();
   abstract public function getAdapterApplicationClass();
   abstract public function getObject();
 
 
 /* -(  Fields  )------------------------------------------------------------- */
 
 
   public function getFields() {
     return array(
       self::FIELD_ALWAYS,
     );
   }
 
   public function getFieldNameMap() {
     return array(
       self::FIELD_TITLE => pht('Title'),
       self::FIELD_BODY => pht('Body'),
       self::FIELD_AUTHOR => pht('Author'),
       self::FIELD_COMMITTER => pht('Committer'),
       self::FIELD_REVIEWER => pht('Reviewer'),
       self::FIELD_REVIEWERS => pht('Reviewers'),
       self::FIELD_CC => pht('CCs'),
       self::FIELD_TAGS => pht('Tags'),
       self::FIELD_DIFF_FILE => pht('Any changed filename'),
       self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
       self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
       self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
       self::FIELD_REPOSITORY => pht('Repository'),
       self::FIELD_RULE => pht('Another Herald rule'),
       self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
       self::FIELD_AFFECTED_PACKAGE_OWNER =>
         pht("Any affected package's owner"),
       self::FIELD_CONTENT_SOURCE => pht('Content Source'),
       self::FIELD_ALWAYS => pht('Always'),
       self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
     );
   }
 
 
 /* -(  Conditions  )--------------------------------------------------------- */
 
 
   public function getConditionNameMap() {
     return array(
       self::CONDITION_CONTAINS        => pht('contains'),
       self::CONDITION_NOT_CONTAINS    => pht('does not contain'),
       self::CONDITION_IS              => pht('is'),
       self::CONDITION_IS_NOT          => pht('is not'),
       self::CONDITION_IS_ANY          => pht('is any of'),
       self::CONDITION_IS_NOT_ANY      => pht('is not any of'),
       self::CONDITION_INCLUDE_ALL     => pht('include all of'),
       self::CONDITION_INCLUDE_ANY     => pht('include any of'),
       self::CONDITION_INCLUDE_NONE    => pht('do not include'),
       self::CONDITION_IS_ME           => pht('is myself'),
       self::CONDITION_IS_NOT_ME       => pht('is not myself'),
       self::CONDITION_REGEXP          => pht('matches regexp'),
       self::CONDITION_RULE            => pht('matches:'),
       self::CONDITION_NOT_RULE        => pht('does not match:'),
       self::CONDITION_EXISTS          => pht('exists'),
       self::CONDITION_NOT_EXISTS      => pht('does not exist'),
       self::CONDITION_UNCONDITIONALLY => '',  // don't show anything!
       self::CONDITION_REGEXP_PAIR     => pht('matches regexp pair'),
     );
   }
 
   public function getConditionsForField($field) {
     switch ($field) {
       case self::FIELD_TITLE:
       case self::FIELD_BODY:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_NOT_CONTAINS,
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
           self::CONDITION_REGEXP,
         );
       case self::FIELD_AUTHOR:
       case self::FIELD_COMMITTER:
       case self::FIELD_REPOSITORY:
       case self::FIELD_REVIEWER:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
         );
       case self::FIELD_TAGS:
       case self::FIELD_REVIEWERS:
       case self::FIELD_CC:
       case self::FIELD_AUTHOR_PROJECTS:
         return array(
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_DIFF_FILE:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_REGEXP,
         );
       case self::FIELD_DIFF_CONTENT:
       case self::FIELD_DIFF_ADDED_CONTENT:
       case self::FIELD_DIFF_REMOVED_CONTENT:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_REGEXP,
           self::CONDITION_REGEXP_PAIR,
         );
       case self::FIELD_RULE:
         return array(
           self::CONDITION_RULE,
           self::CONDITION_NOT_RULE,
         );
       case self::FIELD_AFFECTED_PACKAGE:
       case self::FIELD_AFFECTED_PACKAGE_OWNER:
         return array(
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_CONTENT_SOURCE:
         return array(
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
         );
       case self::FIELD_ALWAYS:
         return array(
           self::CONDITION_UNCONDITIONALLY,
         );
       default:
         throw new Exception(
           "This adapter does not define conditions for field '{$field}'!");
     }
   }
 
   public function doesConditionMatch(
     HeraldEngine $engine,
     HeraldRule $rule,
     HeraldCondition $condition,
     $field_value) {
 
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
 
     switch ($condition_type) {
       case self::CONDITION_CONTAINS:
         // "Contains" can take an array of strings, as in "Any changed
         // filename" for diffs.
         foreach ((array)$field_value as $value) {
           if (stripos($value, $condition_value) !== false) {
             return true;
           }
         }
         return false;
       case self::CONDITION_NOT_CONTAINS:
         return (stripos($field_value, $condition_value) === false);
       case self::CONDITION_IS:
         return ($field_value == $condition_value);
       case self::CONDITION_IS_NOT:
         return ($field_value != $condition_value);
       case self::CONDITION_IS_ME:
         return ($field_value == $rule->getAuthorPHID());
       case self::CONDITION_IS_NOT_ME:
         return ($field_value != $rule->getAuthorPHID());
       case self::CONDITION_IS_ANY:
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             "Expected condition value to be an array.");
         }
         $condition_value = array_fuse($condition_value);
         return isset($condition_value[$field_value]);
       case self::CONDITION_IS_NOT_ANY:
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             "Expected condition value to be an array.");
         }
         $condition_value = array_fuse($condition_value);
         return !isset($condition_value[$field_value]);
       case self::CONDITION_INCLUDE_ALL:
         if (!is_array($field_value)) {
           throw new HeraldInvalidConditionException(
             "Object produced non-array value!");
         }
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             "Expected condition value to be an array.");
         }
 
         $have = array_select_keys(array_fuse($field_value), $condition_value);
         return (count($have) == count($condition_value));
       case self::CONDITION_INCLUDE_ANY:
         return (bool)array_select_keys(
           array_fuse($field_value),
           $condition_value);
       case self::CONDITION_INCLUDE_NONE:
         return !array_select_keys(
           array_fuse($field_value),
           $condition_value);
       case self::CONDITION_EXISTS:
         return (bool)$field_value;
       case self::CONDITION_NOT_EXISTS:
         return !$field_value;
       case self::CONDITION_UNCONDITIONALLY:
         return (bool)$field_value;
       case self::CONDITION_REGEXP:
         foreach ((array)$field_value as $value) {
           // We add the 'S' flag because we use the regexp multiple times.
           // It shouldn't cause any troubles if the flag is already there
           // - /.*/S is evaluated same as /.*/SS.
           $result = @preg_match($condition_value . 'S', $value);
           if ($result === false) {
             throw new HeraldInvalidConditionException(
               "Regular expression is not valid!");
           }
           if ($result) {
             return true;
           }
         }
         return false;
       case self::CONDITION_REGEXP_PAIR:
         // Match a JSON-encoded pair of regular expressions against a
         // dictionary. The first regexp must match the dictionary key, and the
         // second regexp must match the dictionary value. If any key/value pair
         // in the dictionary matches both regexps, the condition is satisfied.
         $regexp_pair = json_decode($condition_value, true);
         if (!is_array($regexp_pair)) {
           throw new HeraldInvalidConditionException(
             "Regular expression pair is not valid JSON!");
         }
         if (count($regexp_pair) != 2) {
           throw new HeraldInvalidConditionException(
             "Regular expression pair is not a pair!");
         }
 
         $key_regexp   = array_shift($regexp_pair);
         $value_regexp = array_shift($regexp_pair);
 
         foreach ((array)$field_value as $key => $value) {
           $key_matches = @preg_match($key_regexp, $key);
           if ($key_matches === false) {
             throw new HeraldInvalidConditionException(
               "First regular expression is invalid!");
           }
           if ($key_matches) {
             $value_matches = @preg_match($value_regexp, $value);
             if ($value_matches === false) {
               throw new HeraldInvalidConditionException(
                 "Second regular expression is invalid!");
             }
             if ($value_matches) {
               return true;
             }
           }
         }
         return false;
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
         $rule = $engine->getRule($condition_value);
         if (!$rule) {
           throw new HeraldInvalidConditionException(
             "Condition references a rule which does not exist!");
         }
 
         $is_not = ($condition_type == self::CONDITION_NOT_RULE);
         $result = $engine->doesRuleMatch($rule, $this);
         if ($is_not) {
           $result = !$result;
         }
         return $result;
       default:
         throw new HeraldInvalidConditionException(
           "Unknown condition '{$condition_type}'.");
     }
   }
 
   public function willSaveCondition(HeraldCondition $condition) {
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
 
     switch ($condition_type) {
       case self::CONDITION_REGEXP:
         $ok = @preg_match($condition_value, '');
         if ($ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression "%s" is not valid. Regular expressions '.
               'must have enclosing characters (e.g. "@/path/to/file@", not '.
               '"/path/to/file") and be syntactically correct.',
               $condition_value));
         }
         break;
       case self::CONDITION_REGEXP_PAIR:
         $json = json_decode($condition_value, true);
         if (!is_array($json)) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" is not valid JSON. Enter a '.
               'valid JSON array with two elements.',
               $condition_value));
         }
 
         if (count($json) != 2) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" must have exactly two '.
               'elements.',
               $condition_value));
         }
 
         $key_regexp = array_shift($json);
         $val_regexp = array_shift($json);
 
         $key_ok = @preg_match($key_regexp, '');
         if ($key_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The first regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $key_regexp));
         }
 
         $val_ok = @preg_match($val_regexp, '');
         if ($val_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The second regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $val_regexp));
         }
         break;
       case self::CONDITION_CONTAINS:
       case self::CONDITION_NOT_CONTAINS:
       case self::CONDITION_IS:
       case self::CONDITION_IS_NOT:
       case self::CONDITION_IS_ANY:
       case self::CONDITION_IS_NOT_ANY:
       case self::CONDITION_INCLUDE_ALL:
       case self::CONDITION_INCLUDE_ANY:
       case self::CONDITION_INCLUDE_NONE:
       case self::CONDITION_IS_ME:
       case self::CONDITION_IS_NOT_ME:
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
       case self::CONDITION_EXISTS:
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_UNCONDITIONALLY:
         // No explicit validation for these types, although there probably
         // should be in some cases.
         break;
       default:
         throw new HeraldInvalidConditionException(
           pht(
             'Unknown condition "%s"!',
             $condition_type));
     }
   }
 
 
 /* -(  Actions  )------------------------------------------------------------ */
 
   abstract public function getActions($rule_type);
 
   public function getActionNameMap($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
         return array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add emails to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove emails from CC'),
           self::ACTION_EMAIL        => pht('Send an email to'),
           self::ACTION_AUDIT        => pht('Trigger an Audit by'),
           self::ACTION_FLAG         => pht('Mark with flag'),
           self::ACTION_ASSIGN_TASK  => pht('Assign task to'),
           self::ACTION_ADD_PROJECTS => pht('Add projects'),
           self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
           self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
+          self::ACTION_APPLY_BUILD_PLANS => pht('Apply build plans'),
         );
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add me to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove me from CC'),
           self::ACTION_EMAIL        => pht('Send me an email'),
           self::ACTION_AUDIT        => pht('Trigger an Audit by me'),
           self::ACTION_FLAG         => pht('Mark with flag'),
           self::ACTION_ASSIGN_TASK  => pht('Assign task to me'),
           self::ACTION_ADD_PROJECTS => pht('Add projects'),
           self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
           self::ACTION_ADD_BLOCKING_REVIEWERS =>
             pht('Add me as a blocking reviewer'),
         );
       default:
         throw new Exception("Unknown rule type '{$rule_type}'!");
     }
   }
 
   public function willSaveAction(
     HeraldRule $rule,
     HeraldAction $action) {
 
     $target = $action->getTarget();
     if (is_array($target)) {
       $target = array_keys($target);
     }
 
     $author_phid = $rule->getAuthorPHID();
 
     $rule_type = $rule->getRuleType();
     if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
       switch ($action->getAction()) {
         case self::ACTION_EMAIL:
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_AUDIT:
         case self::ACTION_ASSIGN_TASK:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           // For personal rules, force these actions to target the rule owner.
           $target = array($author_phid);
           break;
         case self::ACTION_FLAG:
           // Make sure flag color is valid; set to blue if not.
           $color_map = PhabricatorFlagColor::getColorNameMap();
           if (empty($color_map[$target])) {
             $target = PhabricatorFlagColor::COLOR_BLUE;
           }
           break;
         case self::ACTION_NOTHING:
           break;
         default:
           throw new HeraldInvalidActionException(
             pht(
               'Unrecognized action type "%s"!',
               $action->getAction()));
       }
     }
 
     $action->setTarget($target);
   }
 
 
 
 /* -(  Values  )------------------------------------------------------------- */
 
 
   public function getValueTypeForFieldAndCondition($field, $condition) {
     switch ($condition) {
       case self::CONDITION_CONTAINS:
       case self::CONDITION_NOT_CONTAINS:
       case self::CONDITION_REGEXP:
       case self::CONDITION_REGEXP_PAIR:
         return self::VALUE_TEXT;
       case self::CONDITION_IS:
       case self::CONDITION_IS_NOT:
         switch ($field) {
           case self::FIELD_CONTENT_SOURCE:
             return self::VALUE_CONTENT_SOURCE;
           default:
             return self::VALUE_TEXT;
         }
         break;
       case self::CONDITION_IS_ANY:
       case self::CONDITION_IS_NOT_ANY:
         switch ($field) {
           case self::FIELD_REPOSITORY:
             return self::VALUE_REPOSITORY;
           default:
             return self::VALUE_USER;
         }
         break;
       case self::CONDITION_INCLUDE_ALL:
       case self::CONDITION_INCLUDE_ANY:
       case self::CONDITION_INCLUDE_NONE:
         switch ($field) {
           case self::FIELD_REPOSITORY:
             return self::VALUE_REPOSITORY;
           case self::FIELD_CC:
             return self::VALUE_EMAIL;
           case self::FIELD_TAGS:
             return self::VALUE_TAG;
           case self::FIELD_AFFECTED_PACKAGE:
             return self::VALUE_OWNERS_PACKAGE;
           case self::FIELD_AUTHOR_PROJECTS:
             return self::VALUE_PROJECT;
           case self::FIELD_REVIEWERS:
             return self::VALUE_USER_OR_PROJECT;
           default:
             return self::VALUE_USER;
         }
         break;
       case self::CONDITION_IS_ME:
       case self::CONDITION_IS_NOT_ME:
       case self::CONDITION_EXISTS:
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_UNCONDITIONALLY:
         return self::VALUE_NONE;
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
         return self::VALUE_RULE;
       default:
         throw new Exception("Unknown condition '{$condition}'.");
     }
   }
 
   public static function getValueTypeForAction($action, $rule_type) {
     $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
 
     if ($is_personal) {
       switch ($action) {
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_EMAIL:
         case self::ACTION_NOTHING:
         case self::ACTION_AUDIT:
         case self::ACTION_ASSIGN_TASK:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           return self::VALUE_NONE;
         case self::ACTION_FLAG:
           return self::VALUE_FLAG_COLOR;
         case self::ACTION_ADD_PROJECTS:
           return self::VALUE_PROJECT;
         default:
           throw new Exception("Unknown or invalid action '{$action}'.");
       }
     } else {
       switch ($action) {
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_EMAIL:
           return self::VALUE_EMAIL;
         case self::ACTION_NOTHING:
           return self::VALUE_NONE;
         case self::ACTION_AUDIT:
         case self::ACTION_ADD_PROJECTS:
           return self::VALUE_PROJECT;
         case self::ACTION_FLAG:
           return self::VALUE_FLAG_COLOR;
         case self::ACTION_ASSIGN_TASK:
           return self::VALUE_USER;
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           return self::VALUE_USER_OR_PROJECT;
+        case self::ACTION_APPLY_BUILD_PLANS:
+          return self::VALUE_BUILD_PLAN;
         default:
           throw new Exception("Unknown or invalid action '{$action}'.");
       }
     }
   }
 
 
 /* -(  Repetition  )--------------------------------------------------------- */
 
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
     );
   }
 
 
   public static function applyFlagEffect(HeraldEffect $effect, $phid) {
     $color = $effect->getTarget();
 
     // TODO: Silly that we need to load this again here.
     $rule = id(new HeraldRule())->load($effect->getRuleID());
     $user = id(new PhabricatorUser())->loadOneWhere(
       'phid = %s',
       $rule->getAuthorPHID());
 
     $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
     if ($flag) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht('Object already flagged.'));
     }
 
     $handle = id(new PhabricatorHandleQuery())
       ->setViewer($user)
       ->withPHIDs(array($phid))
       ->executeOne();
 
     $flag = new PhabricatorFlag();
     $flag->setOwnerPHID($user->getPHID());
     $flag->setType($handle->getType());
     $flag->setObjectPHID($handle->getPHID());
 
     // TOOD: Should really be transcript PHID, but it doesn't exist yet.
     $flag->setReasonPHID($user->getPHID());
 
     $flag->setColor($color);
     $flag->setNote(
       pht('Flagged by Herald Rule "%s".', $rule->getName()));
     $flag->save();
 
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Added flag.'));
   }
 
   public static function getAllAdapters() {
     static $adapters;
     if (!$adapters) {
       $adapters = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
     }
     return $adapters;
   }
 
   public static function getAdapterForContentType($content_type) {
     $adapters = self::getAllAdapters();
 
     foreach ($adapters as $adapter) {
       if ($adapter->getAdapterContentType() == $content_type) {
         return $adapter;
       }
     }
 
     throw new Exception(
       pht(
         'No adapter exists for Herald content type "%s".',
         $content_type));
   }
 
   public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
     $map = array();
 
     $adapters = HeraldAdapter::getAllAdapters();
     foreach ($adapters as $adapter) {
       if (!$adapter->isAvailableToUser($viewer)) {
         continue;
       }
       $type = $adapter->getAdapterContentType();
       $name = $adapter->getAdapterContentName();
       $map[$type] = $name;
     }
 
     asort($map);
     return $map;
   }
 
   public function renderRuleAsText(HeraldRule $rule, array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
 
     $out = array();
 
     if ($rule->getMustMatchAll()) {
       $out[] = pht('When all of these conditions are met:');
     } else {
       $out[] = pht('When any of these conditions are met:');
     }
 
     $out[] = null;
     foreach ($rule->getConditions() as $condition) {
       $out[] = $this->renderConditionAsText($condition, $handles);
     }
     $out[] = null;
 
     $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
       HeraldRepetitionPolicyConfig::EVERY);
 
     if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
       $out[] = pht('Take these actions every time this rule matches:');
     } else {
       $out[] = pht('Take these actions the first time this rule matches:');
     }
 
     $out[] = null;
     foreach ($rule->getActions() as $action) {
       $out[] = $this->renderActionAsText($action, $handles);
     }
 
     return phutil_implode_html("\n", $out);
   }
 
   private function renderConditionAsText(
     HeraldCondition $condition,
     array $handles) {
     $field_type = $condition->getFieldName();
     $field_name = idx($this->getFieldNameMap(), $field_type);
 
     $condition_type = $condition->getFieldCondition();
     $condition_name = idx($this->getConditionNameMap(), $condition_type);
 
     $value = $this->renderConditionValueAsText($condition, $handles);
 
     return hsprintf('    %s %s %s', $field_name, $condition_name, $value);
   }
 
   private function renderActionAsText(
     HeraldAction $action,
     array $handles) {
     $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
 
     $action_type = $action->getAction();
     $action_name = idx($this->getActionNameMap($rule_global), $action_type);
 
     $target = $this->renderActionTargetAsText($action, $handles);
 
     return hsprintf('    %s %s', $action_name, $target);
   }
 
   private function renderConditionValueAsText(
     HeraldCondition $condition,
     array $handles) {
 
     $value = $condition->getValue();
     if (!is_array($value)) {
       $value = array($value);
     }
     foreach ($value as $index => $val) {
       $handle = idx($handles, $val);
       if ($handle) {
         $value[$index] = $handle->renderLink();
       }
     }
     $value = phutil_implode_html(', ', $value);
     return $value;
   }
 
   private function renderActionTargetAsText(
     HeraldAction $action,
     array $handles) {
 
     $target = $action->getTarget();
     if (!is_array($target)) {
       $target = array($target);
     }
     foreach ($target as $index => $val) {
       $handle = idx($handles, $val);
       if ($handle) {
         $target[$index] = $handle->renderLink();
       }
     }
     $target = phutil_implode_html(', ', $target);
     return $target;
   }
 
   /**
    * Given a @{class:HeraldRule}, this function extracts all the phids that
    * we'll want to load as handles later.
    *
    * This function performs a somewhat hacky approach to figuring out what
    * is and is not a phid - try to get the phid type and if the type is
    * *not* unknown assume its a valid phid.
    *
    * Don't try this at home. Use more strongly typed data at home.
    *
    * Think of the children.
    */
   public static function getHandlePHIDs(HeraldRule $rule) {
     $phids = array($rule->getAuthorPHID());
     foreach ($rule->getConditions() as $condition) {
       $value = $condition->getValue();
       if (!is_array($value)) {
         $value = array($value);
       }
       foreach ($value as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
 
     foreach ($rule->getActions() as $action) {
       $target = $action->getTarget();
       if (!is_array($target)) {
         $target = array($target);
       }
       foreach ($target as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
     return $phids;
   }
 
 }
 
diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php
index 5b56446cc..2c9d7090d 100644
--- a/src/applications/herald/adapter/HeraldCommitAdapter.php
+++ b/src/applications/herald/adapter/HeraldCommitAdapter.php
@@ -1,436 +1,451 @@
 <?php
 
 /**
  * @group herald
  */
 final class HeraldCommitAdapter extends HeraldAdapter {
 
   const FIELD_NEED_AUDIT_FOR_PACKAGE      = 'need-audit-for-package';
   const FIELD_DIFFERENTIAL_REVISION       = 'differential-revision';
   const FIELD_DIFFERENTIAL_REVIEWERS      = 'differential-reviewers';
   const FIELD_DIFFERENTIAL_CCS            = 'differential-ccs';
   const FIELD_REPOSITORY_AUTOCLOSE_BRANCH = 'repository-autoclose-branch';
 
   protected $diff;
   protected $revision;
 
   protected $repository;
   protected $commit;
   protected $commitData;
   private $commitDiff;
 
   protected $emailPHIDs = array();
   protected $addCCPHIDs = array();
   protected $auditMap = array();
+  protected $buildPlans = array();
 
   protected $affectedPaths;
   protected $affectedRevision;
   protected $affectedPackages;
   protected $auditNeededPackages;
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorApplicationDiffusion';
   }
 
   public function getObject() {
     return $this->commit;
   }
 
   public function getAdapterContentType() {
     return 'commit';
   }
 
   public function getAdapterContentName() {
     return pht('Commits');
   }
 
   public function getFieldNameMap() {
     return array(
       self::FIELD_NEED_AUDIT_FOR_PACKAGE =>
         pht('Affected packages that need audit'),
       self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
       self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
       self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
       self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH => pht('On autoclose branch'),
     ) + parent::getFieldNameMap();
   }
 
   public function getFields() {
     return array_merge(
       array(
         self::FIELD_BODY,
         self::FIELD_AUTHOR,
         self::FIELD_COMMITTER,
         self::FIELD_REVIEWER,
         self::FIELD_REPOSITORY,
         self::FIELD_DIFF_FILE,
         self::FIELD_DIFF_CONTENT,
         self::FIELD_DIFF_ADDED_CONTENT,
         self::FIELD_DIFF_REMOVED_CONTENT,
         self::FIELD_RULE,
         self::FIELD_AFFECTED_PACKAGE,
         self::FIELD_AFFECTED_PACKAGE_OWNER,
         self::FIELD_NEED_AUDIT_FOR_PACKAGE,
         self::FIELD_DIFFERENTIAL_REVISION,
         self::FIELD_DIFFERENTIAL_REVIEWERS,
         self::FIELD_DIFFERENTIAL_CCS,
         self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH,
       ),
       parent::getFields());
   }
 
   public function getConditionsForField($field) {
     switch ($field) {
       case self::FIELD_DIFFERENTIAL_REVIEWERS:
         return array(
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_DIFFERENTIAL_CCS:
         return array(
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_DIFFERENTIAL_REVISION:
         return array(
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
         return array(
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH:
         return array(
           self::CONDITION_UNCONDITIONALLY,
         );
     }
     return parent::getConditionsForField($field);
   }
 
   public function getActions($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
         return array(
           self::ACTION_ADD_CC,
           self::ACTION_EMAIL,
           self::ACTION_AUDIT,
-          self::ACTION_NOTHING,
+          self::ACTION_APPLY_BUILD_PLANS,
+          self::ACTION_NOTHING
         );
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array(
           self::ACTION_ADD_CC,
           self::ACTION_EMAIL,
           self::ACTION_FLAG,
           self::ACTION_AUDIT,
           self::ACTION_NOTHING,
         );
     }
   }
 
   public function getValueTypeForFieldAndCondition($field, $condition) {
     switch ($field) {
       case self::FIELD_DIFFERENTIAL_CCS:
         return self::VALUE_EMAIL;
       case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
         return self::VALUE_OWNERS_PACKAGE;
     }
 
     return parent::getValueTypeForFieldAndCondition($field, $condition);
   }
 
   public static function newLegacyAdapter(
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit,
     PhabricatorRepositoryCommitData $commit_data) {
 
     $object = new HeraldCommitAdapter();
 
     $commit->attachRepository($repository);
 
     $object->repository = $repository;
     $object->commit = $commit;
     $object->commitData = $commit_data;
 
     return $object;
   }
 
   public function getPHID() {
     return $this->commit->getPHID();
   }
 
   public function getEmailPHIDs() {
     return array_keys($this->emailPHIDs);
   }
 
   public function getAddCCMap() {
     return $this->addCCPHIDs;
   }
 
   public function getAuditMap() {
     return $this->auditMap;
   }
 
+  public function getBuildPlans() {
+    return $this->buildPlans;
+  }
+
   public function getHeraldName() {
     return
       'r'.
       $this->repository->getCallsign().
       $this->commit->getCommitIdentifier();
   }
 
   public function loadAffectedPaths() {
     if ($this->affectedPaths === null) {
       $result = PhabricatorOwnerPathQuery::loadAffectedPaths(
         $this->repository,
         $this->commit,
         PhabricatorUser::getOmnipotentUser());
       $this->affectedPaths = $result;
     }
     return $this->affectedPaths;
   }
 
   public function loadAffectedPackages() {
     if ($this->affectedPackages === null) {
       $packages = PhabricatorOwnersPackage::loadAffectedPackages(
         $this->repository,
         $this->loadAffectedPaths());
       $this->affectedPackages = $packages;
     }
     return $this->affectedPackages;
   }
 
   public function loadAuditNeededPackage() {
     if ($this->auditNeededPackages === null) {
       $status_arr = array(
         PhabricatorAuditStatusConstants::AUDIT_REQUIRED,
         PhabricatorAuditStatusConstants::CONCERNED,
       );
       $requests = id(new PhabricatorRepositoryAuditRequest())
           ->loadAllWhere(
         "commitPHID = %s AND auditStatus IN (%Ls)",
         $this->commit->getPHID(),
         $status_arr);
 
       $packages = mpull($requests, 'getAuditorPHID');
       $this->auditNeededPackages = $packages;
     }
     return $this->auditNeededPackages;
   }
 
   public function loadDifferentialRevision() {
     if ($this->affectedRevision === null) {
       $this->affectedRevision = false;
       $data = $this->commitData;
       $revision_id = $data->getCommitDetail('differential.revisionID');
       if ($revision_id) {
         // NOTE: The Herald rule owner might not actually have access to
         // the revision, and can control which revision a commit is
         // associated with by putting text in the commit message. However,
         // the rules they can write against revisions don't actually expose
         // anything interesting, so it seems reasonable to load unconditionally
         // here.
 
         $revision = id(new DifferentialRevisionQuery())
           ->withIDs(array($revision_id))
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->needRelationships(true)
           ->needReviewerStatus(true)
           ->executeOne();
         if ($revision) {
           $this->affectedRevision = $revision;
         }
       }
     }
     return $this->affectedRevision;
   }
 
   private function loadCommitDiff() {
     $drequest = DiffusionRequest::newFromDictionary(
       array(
         'user' => PhabricatorUser::getOmnipotentUser(),
         'repository' => $this->repository,
         'commit' => $this->commit->getCommitIdentifier(),
       ));
 
     $raw = DiffusionQuery::callConduitWithDiffusionRequest(
       PhabricatorUser::getOmnipotentUser(),
       $drequest,
       'diffusion.rawdiffquery',
       array(
         'commit' => $this->commit->getCommitIdentifier(),
         'timeout' => 60 * 60 * 15,
         'linesOfContext' => 0));
 
     $parser = new ArcanistDiffParser();
     $changes = $parser->parseDiff($raw);
 
     $diff = DifferentialDiff::newFromRawChanges($changes);
     return $diff;
   }
 
   private function getDiffContent($type) {
     if ($this->commitDiff === null) {
       try {
         $this->commitDiff = $this->loadCommitDiff();
       } catch (Exception $ex) {
         $this->commitDiff = $ex;
         phlog($ex);
       }
     }
 
     if ($this->commitDiff instanceof Exception) {
       $ex = $this->commitDiff;
       $ex_class = get_class($ex);
       $ex_message = pht('Failed to load changes: %s', $ex->getMessage());
 
       return array(
         '<'.$ex_class.'>' => $ex_message,
       );
     }
 
     $changes = $this->commitDiff->getChangesets();
 
     $result = array();
     foreach ($changes as $change) {
       $lines = array();
       foreach ($change->getHunks() as $hunk) {
         switch ($type) {
           case '-':
             $lines[] = $hunk->makeOldFile();
             break;
           case '+':
             $lines[] = $hunk->makeNewFile();
             break;
           case '*':
             $lines[] = $hunk->makeChanges();
             break;
           default:
             throw new Exception("Unknown content selection '{$type}'!");
         }
       }
       $result[$change->getFilename()] = implode("\n", $lines);
     }
 
     return $result;
   }
 
   public function getHeraldField($field) {
     $data = $this->commitData;
     switch ($field) {
       case self::FIELD_BODY:
         return $data->getCommitMessage();
       case self::FIELD_AUTHOR:
         return $data->getCommitDetail('authorPHID');
       case self::FIELD_COMMITTER:
         return $data->getCommitDetail('committerPHID');
       case self::FIELD_REVIEWER:
         return $data->getCommitDetail('reviewerPHID');
       case self::FIELD_DIFF_FILE:
         return $this->loadAffectedPaths();
       case self::FIELD_REPOSITORY:
         return $this->repository->getPHID();
       case self::FIELD_DIFF_CONTENT:
         return $this->getDiffContent('*');
       case self::FIELD_DIFF_ADDED_CONTENT:
         return $this->getDiffContent('+');
       case self::FIELD_DIFF_REMOVED_CONTENT:
         return $this->getDiffContent('-');
       case self::FIELD_AFFECTED_PACKAGE:
         $packages = $this->loadAffectedPackages();
         return mpull($packages, 'getPHID');
       case self::FIELD_AFFECTED_PACKAGE_OWNER:
         $packages = $this->loadAffectedPackages();
         $owners = PhabricatorOwnersOwner::loadAllForPackages($packages);
         return mpull($owners, 'getUserPHID');
       case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
         return $this->loadAuditNeededPackage();
       case self::FIELD_DIFFERENTIAL_REVISION:
         $revision = $this->loadDifferentialRevision();
         if (!$revision) {
           return null;
         }
         return $revision->getID();
       case self::FIELD_DIFFERENTIAL_REVIEWERS:
         $revision = $this->loadDifferentialRevision();
         if (!$revision) {
           return array();
         }
         return $revision->getReviewers();
       case self::FIELD_DIFFERENTIAL_CCS:
         $revision = $this->loadDifferentialRevision();
         if (!$revision) {
           return array();
         }
         return $revision->getCCPHIDs();
       case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH:
         return $this->repository->shouldAutocloseCommit(
           $this->commit,
           $this->commitData);
     }
 
     return parent::getHeraldField($field);
   }
 
   public function applyHeraldEffects(array $effects) {
     assert_instances_of($effects, 'HeraldEffect');
 
     $result = array();
     foreach ($effects as $effect) {
       $action = $effect->getAction();
       switch ($action) {
         case self::ACTION_NOTHING:
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Great success at doing nothing.'));
           break;
         case self::ACTION_EMAIL:
           foreach ($effect->getTarget() as $phid) {
             $this->emailPHIDs[$phid] = true;
           }
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Added address to email targets.'));
           break;
         case self::ACTION_ADD_CC:
           foreach ($effect->getTarget() as $phid) {
             if (empty($this->addCCPHIDs[$phid])) {
               $this->addCCPHIDs[$phid] = array();
             }
             $this->addCCPHIDs[$phid][] = $effect->getRuleID();
           }
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Added address to CC.'));
           break;
         case self::ACTION_AUDIT:
           foreach ($effect->getTarget() as $phid) {
             if (empty($this->auditMap[$phid])) {
               $this->auditMap[$phid] = array();
             }
             $this->auditMap[$phid][] = $effect->getRuleID();
           }
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Triggered an audit.'));
           break;
+        case self::ACTION_APPLY_BUILD_PLANS:
+          foreach ($effect->getTarget() as $phid) {
+            $this->buildPlans[] = $phid;
+          }
+          $result[] = new HeraldApplyTranscript(
+            $effect,
+            true,
+            pht('Applied build plans.'));
+          break;
         case self::ACTION_FLAG:
           $result[] = parent::applyFlagEffect(
             $effect,
             $this->commit->getPHID());
           break;
         default:
           throw new Exception("No rules to handle action '{$action}'.");
       }
     }
     return $result;
   }
 }
diff --git a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
index 2b0c7ee39..7a9116d3a 100644
--- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
+++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
@@ -1,477 +1,492 @@
 <?php
 
 final class HeraldDifferentialRevisionAdapter extends HeraldAdapter {
 
   protected $revision;
   protected $diff;
 
   protected $explicitCCs;
   protected $explicitReviewers;
   protected $forbiddenCCs;
 
   protected $newCCs = array();
   protected $remCCs = array();
   protected $emailPHIDs = array();
   protected $addReviewerPHIDs = array();
   protected $blockingReviewerPHIDs = array();
+  protected $buildPlans = array();
 
   protected $repository;
   protected $affectedPackages;
   protected $changesets;
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorApplicationDifferential';
   }
 
   public function getObject() {
     return $this->revision;
   }
 
   public function getAdapterContentType() {
     return 'differential';
   }
 
   public function getAdapterContentName() {
     return pht('Differential Revisions');
   }
 
   public function getFields() {
     return array_merge(
       array(
         self::FIELD_TITLE,
         self::FIELD_BODY,
         self::FIELD_AUTHOR,
         self::FIELD_AUTHOR_PROJECTS,
         self::FIELD_REVIEWERS,
         self::FIELD_CC,
         self::FIELD_REPOSITORY,
         self::FIELD_DIFF_FILE,
         self::FIELD_DIFF_CONTENT,
         self::FIELD_DIFF_ADDED_CONTENT,
         self::FIELD_DIFF_REMOVED_CONTENT,
         self::FIELD_RULE,
         self::FIELD_AFFECTED_PACKAGE,
         self::FIELD_AFFECTED_PACKAGE_OWNER,
       ),
       parent::getFields());
   }
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
       HeraldRepetitionPolicyConfig::FIRST,
     );
   }
 
   public static function newLegacyAdapter(
     DifferentialRevision $revision,
     DifferentialDiff $diff) {
     $object = new HeraldDifferentialRevisionAdapter();
 
     // Reload the revision to pick up relationship information.
     $revision = id(new DifferentialRevisionQuery())
       ->withIDs(array($revision->getID()))
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->needRelationships(true)
       ->needReviewerStatus(true)
       ->executeOne();
 
     $object->revision = $revision;
     $object->diff = $diff;
 
     return $object;
   }
 
   public function setExplicitCCs($explicit_ccs) {
     $this->explicitCCs = $explicit_ccs;
     return $this;
   }
 
   public function setExplicitReviewers($explicit_reviewers) {
     $this->explicitReviewers = $explicit_reviewers;
     return $this;
   }
 
   public function setForbiddenCCs($forbidden_ccs) {
     $this->forbiddenCCs = $forbidden_ccs;
     return $this;
   }
 
   public function getCCsAddedByHerald() {
     return array_diff_key($this->newCCs, $this->remCCs);
   }
 
   public function getCCsRemovedByHerald() {
     return $this->remCCs;
   }
 
   public function getEmailPHIDsAddedByHerald() {
     return $this->emailPHIDs;
   }
 
   public function getReviewersAddedByHerald() {
     return $this->addReviewerPHIDs;
   }
 
   public function getBlockingReviewersAddedByHerald() {
     return $this->blockingReviewerPHIDs;
   }
 
+  public function getBuildPlans() {
+    return $this->buildPlans;
+  }
+
   public function getPHID() {
     return $this->revision->getPHID();
   }
 
   public function getHeraldName() {
     return $this->revision->getTitle();
   }
 
   public function loadRepository() {
     if ($this->repository === null) {
       $this->repository = false;
 
       // TODO: (T603) Implement policy stuff in Herald.
       $viewer = PhabricatorUser::getOmnipotentUser();
 
       $revision = $this->revision;
       if ($revision->getRepositoryPHID()) {
         $repositories = id(new PhabricatorRepositoryQuery())
           ->setViewer($viewer)
           ->withPHIDs(array($revision->getRepositoryPHID()))
           ->execute();
         if ($repositories) {
           $this->repository = head($repositories);
           return $this->repository;
         }
       }
 
       $repository = id(new DifferentialRepositoryLookup())
         ->setViewer($viewer)
         ->setDiff($this->diff)
         ->lookupRepository();
       if ($repository) {
         $this->repository = $repository;
         return $this->repository;
       }
 
       $repository = false;
     }
     return $this->repository;
   }
 
   protected function loadChangesets() {
     if ($this->changesets === null) {
       $this->changesets = $this->diff->loadChangesets();
     }
     return $this->changesets;
   }
 
   protected function loadAffectedPaths() {
     $changesets = $this->loadChangesets();
 
     $paths = array();
     foreach ($changesets as $changeset) {
       $paths[] = $this->getAbsoluteRepositoryPathForChangeset($changeset);
     }
     return $paths;
   }
 
   protected function getAbsoluteRepositoryPathForChangeset(
     DifferentialChangeset $changeset) {
 
     $repository = $this->loadRepository();
     if (!$repository) {
       return '/'.ltrim($changeset->getFilename(), '/');
     }
 
     $diff = $this->diff;
 
     return $changeset->getAbsoluteRepositoryPath($repository, $diff);
   }
 
   protected function loadContentDictionary() {
     $changesets = $this->loadChangesets();
 
     $hunks = array();
     if ($changesets) {
       $hunks = id(new DifferentialHunk())->loadAllWhere(
         'changesetID in (%Ld)',
         mpull($changesets, 'getID'));
     }
 
     $dict = array();
     $hunks = mgroup($hunks, 'getChangesetID');
     $changesets = mpull($changesets, null, 'getID');
     foreach ($changesets as $id => $changeset) {
       $path = $this->getAbsoluteRepositoryPathForChangeset($changeset);
       $content = array();
       foreach (idx($hunks, $id, array()) as $hunk) {
         $content[] = $hunk->makeChanges();
       }
       $dict[$path] = implode("\n", $content);
     }
 
     return $dict;
   }
 
   protected function loadAddedContentDictionary() {
     $changesets = $this->loadChangesets();
 
     $hunks = array();
     if ($changesets) {
       $hunks = id(new DifferentialHunk())->loadAllWhere(
         'changesetID in (%Ld)',
         mpull($changesets, 'getID'));
     }
 
     $dict = array();
     $hunks = mgroup($hunks, 'getChangesetID');
     $changesets = mpull($changesets, null, 'getID');
     foreach ($changesets as $id => $changeset) {
       $path = $this->getAbsoluteRepositoryPathForChangeset($changeset);
       $content = array();
       foreach (idx($hunks, $id, array()) as $hunk) {
         $content[] = implode('', $hunk->getAddedLines());
       }
       $dict[$path] = implode("\n", $content);
     }
 
     return $dict;
   }
 
   protected function loadRemovedContentDictionary() {
     $changesets = $this->loadChangesets();
 
     $hunks = array();
     if ($changesets) {
       $hunks = id(new DifferentialHunk())->loadAllWhere(
         'changesetID in (%Ld)',
         mpull($changesets, 'getID'));
     }
 
     $dict = array();
     $hunks = mgroup($hunks, 'getChangesetID');
     $changesets = mpull($changesets, null, 'getID');
     foreach ($changesets as $id => $changeset) {
       $path = $this->getAbsoluteRepositoryPathForChangeset($changeset);
       $content = array();
       foreach (idx($hunks, $id, array()) as $hunk) {
         $content[] = implode('', $hunk->getRemovedLines());
       }
       $dict[$path] = implode("\n", $content);
     }
 
     return $dict;
   }
 
   public function loadAffectedPackages() {
     if ($this->affectedPackages === null) {
       $this->affectedPackages = array();
 
       $repository = $this->loadRepository();
       if ($repository) {
         $packages = PhabricatorOwnersPackage::loadAffectedPackages(
           $repository,
           $this->loadAffectedPaths());
         $this->affectedPackages = $packages;
       }
     }
     return $this->affectedPackages;
   }
 
   public function getHeraldField($field) {
     switch ($field) {
       case self::FIELD_TITLE:
         return $this->revision->getTitle();
         break;
       case self::FIELD_BODY:
         return $this->revision->getSummary()."\n".
                $this->revision->getTestPlan();
         break;
       case self::FIELD_AUTHOR:
         return $this->revision->getAuthorPHID();
         break;
       case self::FIELD_AUTHOR_PROJECTS:
         $author_phid = $this->revision->getAuthorPHID();
         if (!$author_phid) {
           return array();
         }
 
         $projects = id(new PhabricatorProjectQuery())
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->withMemberPHIDs(array($author_phid))
           ->execute();
 
         return mpull($projects, 'getPHID');
       case self::FIELD_DIFF_FILE:
         return $this->loadAffectedPaths();
       case self::FIELD_CC:
         if (isset($this->explicitCCs)) {
           return array_keys($this->explicitCCs);
         } else {
           return $this->revision->getCCPHIDs();
         }
       case self::FIELD_REVIEWERS:
         if (isset($this->explicitReviewers)) {
           return array_keys($this->explicitReviewers);
         } else {
           return $this->revision->getReviewers();
         }
       case self::FIELD_REPOSITORY:
         $repository = $this->loadRepository();
         if (!$repository) {
           return null;
         }
         return $repository->getPHID();
       case self::FIELD_DIFF_CONTENT:
         return $this->loadContentDictionary();
       case self::FIELD_DIFF_ADDED_CONTENT:
         return $this->loadAddedContentDictionary();
       case self::FIELD_DIFF_REMOVED_CONTENT:
         return $this->loadRemovedContentDictionary();
       case self::FIELD_AFFECTED_PACKAGE:
         $packages = $this->loadAffectedPackages();
         return mpull($packages, 'getPHID');
       case self::FIELD_AFFECTED_PACKAGE_OWNER:
         $packages = $this->loadAffectedPackages();
         return PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
           mpull($packages, 'getID'));
     }
 
     return parent::getHeraldField($field);
   }
 
   public function getActions($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
         return array(
           self::ACTION_ADD_CC,
           self::ACTION_REMOVE_CC,
           self::ACTION_EMAIL,
           self::ACTION_ADD_REVIEWERS,
           self::ACTION_ADD_BLOCKING_REVIEWERS,
+          self::ACTION_APPLY_BUILD_PLANS,
           self::ACTION_NOTHING,
         );
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array(
           self::ACTION_ADD_CC,
           self::ACTION_REMOVE_CC,
           self::ACTION_EMAIL,
           self::ACTION_FLAG,
           self::ACTION_ADD_REVIEWERS,
           self::ACTION_ADD_BLOCKING_REVIEWERS,
           self::ACTION_NOTHING,
         );
     }
   }
 
   public function applyHeraldEffects(array $effects) {
     assert_instances_of($effects, 'HeraldEffect');
 
     $result = array();
     if ($this->explicitCCs) {
       $effect = new HeraldEffect();
       $effect->setAction(self::ACTION_ADD_CC);
       $effect->setTarget(array_keys($this->explicitCCs));
       $effect->setReason(
         pht('CCs provided explicitly by revision author or carried over '.
         'from a previous version of the revision.'));
       $result[] = new HeraldApplyTranscript(
         $effect,
         true,
         pht('Added addresses to CC list.'));
     }
 
     $forbidden_ccs = array_fill_keys(
       nonempty($this->forbiddenCCs, array()),
       true);
 
     foreach ($effects as $effect) {
       $action = $effect->getAction();
       switch ($action) {
         case self::ACTION_NOTHING:
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('OK, did nothing.'));
           break;
         case self::ACTION_FLAG:
           $result[] = parent::applyFlagEffect(
             $effect,
             $this->revision->getPHID());
           break;
         case self::ACTION_EMAIL:
         case self::ACTION_ADD_CC:
           $op = ($action == self::ACTION_EMAIL) ? 'email' : 'CC';
           $base_target = $effect->getTarget();
           $forbidden = array();
           foreach ($base_target as $key => $fbid) {
             if (isset($forbidden_ccs[$fbid])) {
               $forbidden[] = $fbid;
               unset($base_target[$key]);
             } else {
               if ($action == self::ACTION_EMAIL) {
                 $this->emailPHIDs[$fbid] = true;
               } else {
                 $this->newCCs[$fbid] = true;
               }
             }
           }
 
           if ($forbidden) {
             $failed = clone $effect;
             $failed->setTarget($forbidden);
             if ($base_target) {
               $effect->setTarget($base_target);
               $result[] = new HeraldApplyTranscript(
                 $effect,
                 true,
                 pht('Added these addresses to %s list. '.
                 'Others could not be added.', $op));
             }
             $result[] = new HeraldApplyTranscript(
               $failed,
               false,
               pht('%s forbidden, these addresses have unsubscribed.', $op));
           } else {
             $result[] = new HeraldApplyTranscript(
               $effect,
               true,
               pht('Added addresses to %s list.', $op));
           }
           break;
         case self::ACTION_REMOVE_CC:
           foreach ($effect->getTarget() as $fbid) {
             $this->remCCs[$fbid] = true;
           }
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Removed addresses from CC list.'));
           break;
         case self::ACTION_ADD_REVIEWERS:
           foreach ($effect->getTarget() as $phid) {
             $this->addReviewerPHIDs[$phid] = true;
           }
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Added reviewers.'));
           break;
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           // This adds reviewers normally, it just also marks them blocking.
           foreach ($effect->getTarget() as $phid) {
             $this->addReviewerPHIDs[$phid] = true;
             $this->blockingReviewerPHIDs[$phid] = true;
           }
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Added blocking reviewers.'));
           break;
+        case self::ACTION_APPLY_BUILD_PLANS:
+          foreach ($effect->getTarget() as $phid) {
+            $this->buildPlans[] = $phid;
+          }
+          $result[] = new HeraldApplyTranscript(
+            $effect,
+            true,
+            pht('Applied build plans.'));
+          break;
         default:
           throw new Exception("No rules to handle action '{$action}'.");
       }
     }
     return $result;
   }
 }
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index adfa8fef4..b9e46d58a 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,551 +1,552 @@
 <?php
 
 final class HeraldRuleController extends HeraldController {
 
   private $id;
   private $filter;
 
   public function willProcessRequest(array $data) {
     $this->id = (int)idx($data, 'id');
   }
 
   public function processRequest() {
 
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $content_type_map = HeraldAdapter::getEnabledAdapterMap($user);
     $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
 
     if ($this->id) {
       $id = $this->id;
       $rule = id(new HeraldRuleQuery())
         ->setViewer($user)
         ->withIDs(array($id))
         ->requireCapabilities(
           array(
             PhabricatorPolicyCapability::CAN_VIEW,
             PhabricatorPolicyCapability::CAN_EDIT,
           ))
         ->executeOne();
       if (!$rule) {
         return new Aphront404Response();
       }
       $cancel_uri = $this->getApplicationURI("rule/{$id}/");
     } else {
       $rule = new HeraldRule();
       $rule->setAuthorPHID($user->getPHID());
       $rule->setMustMatchAll(1);
 
       $content_type = $request->getStr('content_type');
       $rule->setContentType($content_type);
 
       $rule_type = $request->getStr('rule_type');
       if (!isset($rule_type_map[$rule_type])) {
         $rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL;
       }
       $rule->setRuleType($rule_type);
 
       $cancel_uri = $this->getApplicationURI();
     }
 
     if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
       $this->requireApplicationCapability(
         HeraldCapabilityManageGlobalRules::CAPABILITY);
     }
 
     $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
 
     $local_version = id(new HeraldRule())->getConfigVersion();
     if ($rule->getConfigVersion() > $local_version) {
       throw new Exception(
         "This rule was created with a newer version of Herald. You can not ".
         "view or edit it in this older version. Upgrade your Phabricator ".
         "deployment.");
     }
 
     // Upgrade rule version to our version, since we might add newly-defined
     // conditions, etc.
     $rule->setConfigVersion($local_version);
 
     $rule_conditions = $rule->loadConditions();
     $rule_actions = $rule->loadActions();
 
     $rule->attachConditions($rule_conditions);
     $rule->attachActions($rule_actions);
 
     $e_name = true;
     $errors = array();
     if ($request->isFormPost() && $request->getStr('save')) {
       list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
       if (!$errors) {
         $id = $rule->getID();
         $uri = $this->getApplicationURI("rule/{$id}/");
         return id(new AphrontRedirectResponse())->setURI($uri);
       }
     }
 
     if ($errors) {
       $error_view = new AphrontErrorView();
       $error_view->setTitle(pht('Form Errors'));
       $error_view->setErrors($errors);
     } else {
       $error_view = null;
     }
 
     $must_match_selector = $this->renderMustMatchSelector($rule);
     $repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
 
     $handles = $this->loadHandlesForRule($rule);
 
     require_celerity_resource('herald-css');
 
     $content_type_name = $content_type_map[$rule->getContentType()];
     $rule_type_name = $rule_type_map[$rule->getRuleType()];
 
     $form = id(new AphrontFormView())
       ->setUser($user)
       ->setID('herald-rule-edit-form')
       ->addHiddenInput('content_type', $rule->getContentType())
       ->addHiddenInput('rule_type', $rule->getRuleType())
       ->addHiddenInput('save', 1)
       ->appendChild(
         // Build this explicitly (instead of using addHiddenInput())
         // so we can add a sigil to it.
         javelin_tag(
           'input',
           array(
             'type'  => 'hidden',
             'name'  => 'rule',
             'sigil' => 'rule',
           )))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Rule Name'))
           ->setName('name')
           ->setError($e_name)
           ->setValue($rule->getName()));
 
     $form
       ->appendChild(
         id(new AphrontFormMarkupControl())
           ->setValue(pht(
             "This %s rule triggers for %s.",
             phutil_tag('strong', array(), $rule_type_name),
             phutil_tag('strong', array(), $content_type_name))))
       ->appendChild(
         id(new AphrontFormInsetView())
           ->setTitle(pht('Conditions'))
           ->setRightButton(javelin_tag(
             'a',
             array(
               'href' => '#',
               'class' => 'button green',
               'sigil' => 'create-condition',
               'mustcapture' => true
             ),
             pht('New Condition')))
           ->setDescription(
             pht('When %s these conditions are met:', $must_match_selector))
           ->setContent(javelin_tag(
             'table',
             array(
               'sigil' => 'rule-conditions',
               'class' => 'herald-condition-table'
             ),
             '')))
       ->appendChild(
         id(new AphrontFormInsetView())
           ->setTitle(pht('Action'))
           ->setRightButton(javelin_tag(
             'a',
             array(
               'href' => '#',
               'class' => 'button green',
               'sigil' => 'create-action',
               'mustcapture' => true,
             ),
             pht('New Action')))
           ->setDescription(pht(
             'Take these actions %s this rule matches:',
             $repetition_selector))
           ->setContent(javelin_tag(
               'table',
               array(
                 'sigil' => 'rule-actions',
                 'class' => 'herald-action-table',
               ),
               '')))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue(pht('Save Rule'))
           ->addCancelButton($cancel_uri));
 
     $this->setupEditorBehavior($rule, $handles, $adapter);
 
     $title = $rule->getID()
         ? pht('Edit Herald Rule')
         : pht('Create Herald Rule');
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText($title)
       ->setFormError($error_view)
       ->setForm($form);
 
     $crumbs = $this
       ->buildApplicationCrumbs()
       ->addCrumb(
         id(new PhabricatorCrumbView())
           ->setName($title));
 
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $form_box,
       ),
       array(
         'title' => pht('Edit Rule'),
         'device' => true,
       ));
   }
 
   private function saveRule(HeraldAdapter $adapter, $rule, $request) {
     $rule->setName($request->getStr('name'));
     $match_all = ($request->getStr('must_match') == 'all');
     $rule->setMustMatchAll((int)$match_all);
 
     $repetition_policy_param = $request->getStr('repetition_policy');
     $rule->setRepetitionPolicy(
       HeraldRepetitionPolicyConfig::toInt($repetition_policy_param));
 
     $e_name = true;
     $errors = array();
 
     if (!strlen($rule->getName())) {
       $e_name = pht("Required");
       $errors[] = pht("Rule must have a name.");
     }
 
     $data = json_decode($request->getStr('rule'), true);
     if (!is_array($data) ||
         !$data['conditions'] ||
         !$data['actions']) {
       throw new Exception("Failed to decode rule data.");
     }
 
     $conditions = array();
     foreach ($data['conditions'] as $condition) {
       if ($condition === null) {
         // We manage this as a sparse array on the client, so may receive
         // NULL if conditions have been removed.
         continue;
       }
 
       $obj = new HeraldCondition();
       $obj->setFieldName($condition[0]);
       $obj->setFieldCondition($condition[1]);
 
       if (is_array($condition[2])) {
         $obj->setValue(array_keys($condition[2]));
       } else {
         $obj->setValue($condition[2]);
       }
 
       try {
         $adapter->willSaveCondition($obj);
       } catch (HeraldInvalidConditionException $ex) {
         $errors[] = $ex->getMessage();
       }
 
       $conditions[] = $obj;
     }
 
     $actions = array();
     foreach ($data['actions'] as $action) {
       if ($action === null) {
         // Sparse on the client; removals can give us NULLs.
         continue;
       }
 
       if (!isset($action[1])) {
         // Legitimate for any action which doesn't need a target, like
         // "Do nothing".
         $action[1] = null;
       }
 
       $obj = new HeraldAction();
       $obj->setAction($action[0]);
       $obj->setTarget($action[1]);
 
       try {
         $adapter->willSaveAction($rule, $obj);
       } catch (HeraldInvalidActionException $ex) {
         $errors[] = $ex;
       }
 
       $actions[] = $obj;
     }
 
     $rule->attachConditions($conditions);
     $rule->attachActions($actions);
 
     if (!$errors) {
       try {
 
         $edit_action = $rule->getID() ? 'edit' : 'create';
 
         $rule->openTransaction();
           $rule->save();
           $rule->saveConditions($conditions);
           $rule->saveActions($actions);
           $rule->logEdit($request->getUser()->getPHID(), $edit_action);
         $rule->saveTransaction();
 
       } catch (AphrontQueryDuplicateKeyException $ex) {
         $e_name = pht("Not Unique");
         $errors[] = pht("Rule name is not unique. Choose a unique name.");
       }
     }
 
     return array($e_name, $errors);
   }
 
   private function setupEditorBehavior(
     HeraldRule $rule,
     array $handles,
     HeraldAdapter $adapter) {
 
     $serial_conditions = array(
       array('default', 'default', ''),
     );
 
     if ($rule->getConditions()) {
       $serial_conditions = array();
       foreach ($rule->getConditions() as $condition) {
 
         $value = $condition->getValue();
         if (is_array($value)) {
           $value_map = array();
           foreach ($value as $k => $fbid) {
             $value_map[$fbid] = $handles[$fbid]->getName();
           }
           $value = $value_map;
         }
 
         $serial_conditions[] = array(
           $condition->getFieldName(),
           $condition->getFieldCondition(),
           $value,
         );
       }
     }
 
     $serial_actions = array(
       array('default', ''),
     );
     if ($rule->getActions()) {
       $serial_actions = array();
       foreach ($rule->getActions() as $action) {
 
         switch ($action->getAction()) {
           case HeraldAdapter::ACTION_FLAG:
             $current_value = $action->getTarget();
             break;
           default:
             $target_map = array();
             foreach ((array)$action->getTarget() as $fbid) {
               $target_map[$fbid] = $handles[$fbid]->getName();
             }
             $current_value = $target_map;
             break;
         }
 
         $serial_actions[] = array(
           $action->getAction(),
           $current_value,
         );
       }
     }
 
     $all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
     $all_rules = mpull($all_rules, 'getName', 'getID');
     asort($all_rules);
 
     $all_fields = $adapter->getFieldNameMap();
     $all_conditions = $adapter->getConditionNameMap();
     $all_actions = $adapter->getActionNameMap($rule->getRuleType());
 
     $fields = $adapter->getFields();
     $field_map = array_select_keys($all_fields, $fields);
 
     $actions = $adapter->getActions($rule->getRuleType());
     $action_map = array_select_keys($all_actions, $actions);
 
     $config_info = array();
     $config_info['fields'] = $field_map;
     $config_info['conditions'] = $all_conditions;
     $config_info['actions'] = $action_map;
 
     foreach ($config_info['fields'] as $field => $name) {
       $field_conditions = $adapter->getConditionsForField($field);
       $config_info['conditionMap'][$field] = $field_conditions;
     }
 
     foreach ($config_info['fields'] as $field => $fname) {
       foreach ($config_info['conditionMap'][$field] as $condition) {
         $value_type = $adapter->getValueTypeForFieldAndCondition(
           $field,
           $condition);
         $config_info['values'][$field][$condition] = $value_type;
       }
     }
 
     $config_info['rule_type'] = $rule->getRuleType();
 
     foreach ($config_info['actions'] as $action => $name) {
       $config_info['targets'][$action] = $adapter->getValueTypeForAction(
         $action,
        $rule->getRuleType());
     }
 
     Javelin::initBehavior(
       'herald-rule-editor',
       array(
         'root' => 'herald-rule-edit-form',
         'conditions' => (object)$serial_conditions,
         'actions' => (object)$serial_actions,
         'template' => $this->buildTokenizerTemplates() + array(
           'rules' => $all_rules,
           'colors' => PhabricatorFlagColor::getColorNameMap(),
           'defaultColor' => PhabricatorFlagColor::COLOR_BLUE,
           'contentSources' => PhabricatorContentSource::getSourceNameMap(),
           'defaultSource' => PhabricatorContentSource::SOURCE_WEB
         ),
         'author' => array($rule->getAuthorPHID() =>
                           $handles[$rule->getAuthorPHID()]->getName()),
         'info' => $config_info,
       ));
   }
 
   private function loadHandlesForRule($rule) {
     $phids = array();
 
     foreach ($rule->getActions() as $action) {
       if (!is_array($action->getTarget())) {
         continue;
       }
       foreach ($action->getTarget() as $target) {
         $target = (array)$target;
         foreach ($target as $phid) {
           $phids[] = $phid;
         }
       }
     }
 
     foreach ($rule->getConditions() as $condition) {
       $value = $condition->getValue();
       if (is_array($value)) {
         foreach ($value as $phid) {
           $phids[] = $phid;
         }
       }
     }
 
     $phids[] = $rule->getAuthorPHID();
 
     return $this->loadViewerHandles($phids);
   }
 
 
   /**
    * Render the selector for the "When (all of | any of) these conditions are
    * met:" element.
    */
   private function renderMustMatchSelector($rule) {
     return AphrontFormSelectControl::renderSelectTag(
       $rule->getMustMatchAll() ? 'all' : 'any',
       array(
         'all' => pht('all of'),
         'any' => pht('any of'),
       ),
       array(
         'name' => 'must_match',
       ));
   }
 
 
   /**
    * Render the selector for "Take these actions (every time | only the first
    * time) this rule matches..." element.
    */
   private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
     $repetition_policy = HeraldRepetitionPolicyConfig::toString(
       $rule->getRepetitionPolicy());
 
     $repetition_options = $adapter->getRepetitionOptions();
     $repetition_names = HeraldRepetitionPolicyConfig::getMap();
     $repetition_map = array_select_keys($repetition_names, $repetition_options);
 
     if (count($repetition_map) < 2) {
       return head($repetition_names);
     } else {
       return AphrontFormSelectControl::renderSelectTag(
         $repetition_policy,
         $repetition_map,
         array(
           'name' => 'repetition_policy',
         ));
     }
   }
 
 
   protected function buildTokenizerTemplates() {
     $template = new AphrontTokenizerTemplateView();
     $template = $template->render();
 
     return array(
       'source' => array(
         'email'       => '/typeahead/common/mailable/',
         'user'        => '/typeahead/common/accounts/',
         'repository'  => '/typeahead/common/repositories/',
         'package'     => '/typeahead/common/packages/',
         'project'     => '/typeahead/common/projects/',
         'userorproject' => '/typeahead/common/accountsorprojects/',
+        'buildplan'   => '/typeahead/common/buildplans/',
       ),
       'markup' => $template,
     );
   }
 
 
   /**
    * Load rules for the "Another Herald rule..." condition dropdown, which
    * allows one rule to depend upon the success or failure of another rule.
    */
   private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
     $viewer = $this->getRequest()->getUser();
 
     // Any rule can depend on a global rule.
     $all_rules = id(new HeraldRuleQuery())
       ->setViewer($viewer)
       ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
       ->withContentTypes(array($rule->getContentType()))
       ->execute();
 
     if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
       // Personal rules may depend upon your other personal rules.
       $all_rules += id(new HeraldRuleQuery())
         ->setViewer($viewer)
         ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
         ->withContentTypes(array($rule->getContentType()))
         ->withAuthorPHIDs(array($rule->getAuthorPHID()))
         ->execute();
     }
 
     // A rule can not depend upon itself.
     unset($all_rules[$rule->getID()]);
 
     return $all_rules;
   }
 
 
 }
diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
index eaa9f682e..3427628f5 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
@@ -1,444 +1,449 @@
 <?php
 
 final class PhabricatorRepositoryCommitHeraldWorker
   extends PhabricatorRepositoryCommitParserWorker {
 
   public function getRequiredLeaseTime() {
     // Herald rules may take a long time to process.
     return 4 * 60 * 60;
   }
 
   public function parseCommit(
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit) {
 
     $result = $this->applyHeraldRules($repository, $commit);
 
     $commit->writeImportStatusFlag(
       PhabricatorRepositoryCommit::IMPORTED_HERALD);
 
     return $result;
   }
 
   private function applyHeraldRules(
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit) {
 
     // Don't take any actions on an importing repository. Principally, this
     // avoids generating thousands of audits or emails when you import an
     // established repository on an existing install.
     if ($repository->isImporting()) {
       return;
     }
 
     $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
       'commitID = %d',
       $commit->getID());
 
     if (!$data) {
       // TODO: Permanent failure.
       return;
     }
 
     $adapter = HeraldCommitAdapter::newLegacyAdapter(
       $repository,
       $commit,
       $data);
 
     $rules = id(new HeraldRuleQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withContentTypes(array($adapter->getAdapterContentType()))
       ->withDisabled(false)
       ->needConditionsAndActions(true)
       ->needAppliedToPHIDs(array($adapter->getPHID()))
       ->needValidateAuthors(true)
       ->execute();
 
     $engine = new HeraldEngine();
 
     $effects = $engine->applyRules($rules, $adapter);
     $engine->applyEffects($effects, $adapter, $rules);
     $xscript = $engine->getTranscript();
 
     $audit_phids = $adapter->getAuditMap();
     $cc_phids = $adapter->getAddCCMap();
     if ($audit_phids || $cc_phids) {
       $this->createAudits($commit, $audit_phids, $cc_phids, $rules);
     }
 
+    HarbormasterBuildable::applyBuildPlans(
+      $commit->getPHID(),
+      $repository->getPHID(),
+      $adapter->getBuildPlans());
+
     $explicit_auditors = $this->createAuditsFromCommitMessage($commit, $data);
 
     if ($repository->getDetail('herald-disabled')) {
       // This just means "disable email"; audits are (mostly) idempotent.
       return;
     }
 
     $this->publishFeedStory($repository, $commit, $data);
 
     $herald_targets = $adapter->getEmailPHIDs();
 
     $email_phids = array_unique(
       array_merge(
         $explicit_auditors,
         array_keys($cc_phids),
         $herald_targets));
     if (!$email_phids) {
       return;
     }
 
     $revision = $adapter->loadDifferentialRevision();
     if ($revision) {
       $name = $revision->getTitle();
     } else {
       $name = $data->getSummary();
     }
 
     $author_phid = $data->getCommitDetail('authorPHID');
     $reviewer_phid = $data->getCommitDetail('reviewerPHID');
 
     $phids = array_filter(
       array(
         $author_phid,
         $reviewer_phid,
         $commit->getPHID(),
       ));
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs($phids)
       ->execute();
 
     $commit_handle = $handles[$commit->getPHID()];
     $commit_name = $commit_handle->getName();
 
     if ($author_phid) {
       $author_name = $handles[$author_phid]->getName();
     } else {
       $author_name = $data->getAuthorName();
     }
 
     if ($reviewer_phid) {
       $reviewer_name = $handles[$reviewer_phid]->getName();
     } else {
       $reviewer_name = null;
     }
 
     $who = implode(', ', array_filter(array($author_name, $reviewer_name)));
 
     $description = $data->getCommitMessage();
 
     $commit_uri = PhabricatorEnv::getProductionURI($commit_handle->getURI());
     $differential = $revision
       ? PhabricatorEnv::getProductionURI('/D'.$revision->getID())
       : 'No revision.';
 
     $files = $adapter->loadAffectedPaths();
     sort($files);
     $files = implode("\n", $files);
 
     $xscript_id = $xscript->getID();
 
     $why_uri = '/herald/transcript/'.$xscript_id.'/';
 
     $reply_handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit(
       $commit);
 
     $template = new PhabricatorMetaMTAMail();
 
     $inline_patch_text = $this->buildPatch($template, $repository, $commit);
 
     $body = new PhabricatorMetaMTAMailBody();
     $body->addRawSection($description);
     $body->addTextSection(pht('DETAILS'), $commit_uri);
     $body->addTextSection(pht('DIFFERENTIAL REVISION'), $differential);
     $body->addTextSection(pht('AFFECTED FILES'), $files);
     $body->addReplySection($reply_handler->getReplyHandlerInstructions());
     $body->addHeraldSection($why_uri);
     $body->addRawSection($inline_patch_text);
     $body = $body->render();
 
     $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
 
     $threading = PhabricatorAuditCommentEditor::getMailThreading(
       $repository,
       $commit);
     list($thread_id, $thread_topic) = $threading;
 
     $template->setRelatedPHID($commit->getPHID());
     $template->setSubject("{$commit_name}: {$name}");
     $template->setSubjectPrefix($prefix);
     $template->setVarySubjectPrefix("[Commit]");
     $template->setBody($body);
     $template->setThreadID($thread_id, $is_new = true);
     $template->addHeader('Thread-Topic', $thread_topic);
     $template->setIsBulk(true);
 
     $template->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader());
     if ($author_phid) {
       $template->setFrom($author_phid);
     }
 
     // TODO: We should verify that each recipient can actually see the
     // commit before sending them email (T603).
 
     $mails = $reply_handler->multiplexMail(
       $template,
       id(new PhabricatorHandleQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs($email_phids)
       ->execute(),
       array());
 
     foreach ($mails as $mail) {
       $mail->saveAndSend();
     }
   }
 
   private function createAudits(
     PhabricatorRepositoryCommit $commit,
     array $map,
     array $ccmap,
     array $rules) {
     assert_instances_of($rules, 'HeraldRule');
 
     $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
       'commitPHID = %s',
       $commit->getPHID());
     $requests = mpull($requests, null, 'getAuditorPHID');
 
     $rules = mpull($rules, null, 'getID');
 
     $maps = array(
       PhabricatorAuditStatusConstants::AUDIT_REQUIRED => $map,
       PhabricatorAuditStatusConstants::CC => $ccmap,
     );
 
     foreach ($maps as $status => $map) {
       foreach ($map as $phid => $rule_ids) {
         $request = idx($requests, $phid);
         if ($request) {
           continue;
         }
         $reasons = array();
         foreach ($rule_ids as $id) {
           $rule_name = '?';
           if ($rules[$id]) {
             $rule_name = $rules[$id]->getName();
           }
           if ($status == PhabricatorAuditStatusConstants::AUDIT_REQUIRED) {
             $reasons[] = pht(
               'Herald Rule #%d "%s" Triggered Audit',
               $id,
               $rule_name);
           } else {
             $reasons[] = pht(
               'Herald Rule #%d "%s" Triggered CC',
               $id,
               $rule_name);
           }
         }
 
         $request = new PhabricatorRepositoryAuditRequest();
         $request->setCommitPHID($commit->getPHID());
         $request->setAuditorPHID($phid);
         $request->setAuditStatus($status);
         $request->setAuditReasons($reasons);
         $request->save();
       }
     }
 
     $commit->updateAuditStatus($requests);
     $commit->save();
   }
 
 
   /**
    * Find audit requests in the "Auditors" field if it is present and trigger
    * explicit audit requests.
    */
   private function createAuditsFromCommitMessage(
     PhabricatorRepositoryCommit $commit,
     PhabricatorRepositoryCommitData $data) {
 
     $message = $data->getCommitMessage();
 
     $matches = null;
     if (!preg_match('/^Auditors:\s*(.*)$/im', $message, $matches)) {
       return array();
     }
 
     $phids = DifferentialFieldSpecification::parseCommitMessageObjectList(
       $matches[1],
       $include_mailables = false,
       $allow_partial = true);
 
     if (!$phids) {
       return array();
     }
 
     $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
       'commitPHID = %s',
       $commit->getPHID());
     $requests = mpull($requests, null, 'getAuditorPHID');
 
     foreach ($phids as $phid) {
       if (isset($requests[$phid])) {
         continue;
       }
 
       $request = new PhabricatorRepositoryAuditRequest();
       $request->setCommitPHID($commit->getPHID());
       $request->setAuditorPHID($phid);
       $request->setAuditStatus(
         PhabricatorAuditStatusConstants::AUDIT_REQUESTED);
       $request->setAuditReasons(
         array(
           'Requested by Author',
         ));
       $request->save();
 
       $requests[$phid] = $request;
     }
 
     $commit->updateAuditStatus($requests);
     $commit->save();
 
     return $phids;
   }
 
   private function publishFeedStory(
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit,
     PhabricatorRepositoryCommitData $data) {
 
     if (time() > $commit->getEpoch() + (24 * 60 * 60)) {
       // Don't publish stories that are more than 24 hours old, to avoid
       // ridiculous levels of feed spam if a repository is imported without
       // disabling feed publishing.
       return;
     }
 
     $author_phid = $commit->getAuthorPHID();
     $committer_phid = $data->getCommitDetail('committerPHID');
 
     $publisher = new PhabricatorFeedStoryPublisher();
     $publisher->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_COMMIT);
     $publisher->setStoryData(
       array(
         'commitPHID'    => $commit->getPHID(),
         'summary'       => $data->getSummary(),
         'authorName'    => $data->getAuthorName(),
         'authorPHID'    => $author_phid,
         'committerName' => $data->getCommitDetail('committer'),
         'committerPHID' => $committer_phid,
       ));
     $publisher->setStoryTime($commit->getEpoch());
     $publisher->setRelatedPHIDs(
       array_filter(
         array(
           $author_phid,
           $committer_phid,
         )));
     if ($author_phid) {
       $publisher->setStoryAuthorPHID($author_phid);
     }
     $publisher->publish();
   }
 
   private function buildPatch(
     PhabricatorMetaMTAMail $template,
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit) {
 
     $attach_key = 'metamta.diffusion.attach-patches';
     $inline_key = 'metamta.diffusion.inline-patches';
 
     $attach_patches = PhabricatorEnv::getEnvConfig($attach_key);
     $inline_patches = PhabricatorEnv::getEnvConfig($inline_key);
 
     if (!$attach_patches && !$inline_patches) {
       return;
     }
 
     $encoding = $repository->getDetail('encoding', 'UTF-8');
 
     $result = null;
     $patch_error = null;
 
     try {
       $raw_patch = $this->loadRawPatchText($repository, $commit);
       if ($attach_patches) {
         $commit_name = $repository->formatCommitName(
           $commit->getCommitIdentifier());
 
         $template->addAttachment(
           new PhabricatorMetaMTAAttachment(
             $raw_patch,
             $commit_name.'.patch',
             'text/x-patch; charset='.$encoding));
       }
     } catch (Exception $ex) {
       phlog($ex);
       $patch_error = 'Unable to generate: '.$ex->getMessage();
     }
 
     if ($patch_error) {
       $result = $patch_error;
     } else if ($inline_patches) {
       $len = substr_count($raw_patch, "\n");
       if ($len <= $inline_patches) {
         // We send email as utf8, so we need to convert the text to utf8 if
         // we can.
         if ($encoding) {
           $raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding);
         }
         $result = phutil_utf8ize($raw_patch);
       }
     }
 
     if ($result) {
       $result = "PATCH\n\n{$result}\n";
     }
 
     return $result;
   }
 
   private function loadRawPatchText(
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit) {
 
     $drequest = DiffusionRequest::newFromDictionary(
       array(
         'user' => PhabricatorUser::getOmnipotentUser(),
         'initFromConduit' => false,
         'repository' => $repository,
         'commit' => $commit->getCommitIdentifier(),
       ));
 
     $raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
     $raw_query->setLinesOfContext(3);
 
     $time_key = 'metamta.diffusion.time-limit';
     $byte_key = 'metamta.diffusion.byte-limit';
     $time_limit = PhabricatorEnv::getEnvConfig($time_key);
     $byte_limit = PhabricatorEnv::getEnvConfig($byte_key);
 
     if ($time_limit) {
       $raw_query->setTimeout($time_limit);
     }
 
     $raw_diff = $raw_query->loadRawDiff();
 
     $size = strlen($raw_diff);
     if ($byte_limit && $size > $byte_limit) {
       $pretty_size = phabricator_format_bytes($size);
       $pretty_limit = phabricator_format_bytes($byte_limit);
       throw new Exception(
         "Patch size of {$pretty_size} exceeds configured byte size limit of ".
         "{$pretty_limit}.");
     }
 
     return $raw_diff;
   }
 
 }
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
index ce2bcce99..d9830ad85 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
@@ -1,380 +1,395 @@
 <?php
 
 final class PhabricatorTypeaheadCommonDatasourceController
   extends PhabricatorTypeaheadDatasourceController {
 
   private $type;
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function willProcessRequest(array $data) {
     $this->type = $data['type'];
   }
 
   public function processRequest() {
 
     $request = $this->getRequest();
     $viewer = $request->getUser();
     $query = $request->getStr('q');
     $raw_query = $request->getStr('raw');
 
     $need_rich_data = false;
 
     $need_users = false;
     $need_agents = false;
     $need_applications = false;
     $need_all_users = false;
     $need_lists = false;
     $need_projs = false;
     $need_repos = false;
     $need_packages = false;
     $need_upforgrabs = false;
     $need_arcanist_projects = false;
     $need_noproject = false;
     $need_symbols = false;
     $need_jump_objects = false;
+    $need_build_plans = false;
     switch ($this->type) {
       case 'mainsearch':
         $need_users = true;
         $need_applications = true;
         $need_rich_data = true;
         $need_symbols = true;
         $need_projs = true;
         $need_jump_objects = true;
         break;
       case 'searchowner':
         $need_users = true;
         $need_upforgrabs = true;
         break;
       case 'searchproject':
         $need_projs = true;
         $need_noproject = true;
         break;
       case 'users':
         $need_users = true;
         break;
       case 'authors':
         $need_users = true;
         $need_agents = true;
         break;
       case 'mailable':
         $need_users = true;
         $need_lists = true;
         break;
       case 'allmailable':
         $need_users = true;
         $need_all_users = true;
         $need_lists = true;
         break;
       case 'projects':
         $need_projs = true;
         break;
       case 'usersorprojects':
         $need_users = true;
         $need_projs = true;
         break;
       case 'repositories':
         $need_repos = true;
         break;
       case 'packages':
         $need_packages = true;
         break;
       case 'accounts':
         $need_users = true;
         $need_all_users = true;
         break;
       case 'accountsorprojects':
         $need_users = true;
         $need_all_users = true;
         $need_projs = true;
         break;
       case 'arcanistprojects':
         $need_arcanist_projects = true;
         break;
+      case 'buildplans':
+        $need_build_plans = true;
+        break;
     }
 
     $results = array();
 
     if ($need_upforgrabs) {
       $results[] = id(new PhabricatorTypeaheadResult())
         ->setName('upforgrabs (Up For Grabs)')
         ->setPHID(ManiphestTaskOwner::OWNER_UP_FOR_GRABS);
     }
 
     if ($need_noproject) {
       $results[] = id(new PhabricatorTypeaheadResult())
         ->setName('noproject (No Project)')
         ->setPHID(ManiphestTaskOwner::PROJECT_NO_PROJECT);
     }
 
     if ($need_users) {
       $columns = array(
         'isSystemAgent',
         'isAdmin',
         'isDisabled',
         'userName',
         'realName',
         'phid');
 
       if ($query) {
         // This is an arbitrary limit which is just larger than any limit we
         // actually use in the application.
 
         // TODO: The datasource should pass this in the query.
         $limit = 15;
 
         $user_table = new PhabricatorUser();
         $conn_r = $user_table->establishConnection('r');
         $ids = queryfx_all(
           $conn_r,
           'SELECT id FROM %T WHERE username LIKE %>
             ORDER BY username ASC LIMIT %d',
           $user_table->getTableName(),
           $query,
           $limit);
         $ids = ipull($ids, 'id');
 
         if (count($ids) < $limit) {
           // If we didn't find enough username hits, look for real name hits.
           // We need to pull the entire pagesize so that we end up with the
           // right number of items if this query returns many duplicate IDs
           // that we've already selected.
 
           $realname_ids = queryfx_all(
             $conn_r,
             'SELECT DISTINCT userID FROM %T WHERE token LIKE %>
               ORDER BY token ASC LIMIT %d',
             PhabricatorUser::NAMETOKEN_TABLE,
             $query,
             $limit);
           $realname_ids = ipull($realname_ids, 'userID');
           $ids = array_merge($ids, $realname_ids);
 
           $ids = array_unique($ids);
           $ids = array_slice($ids, 0, $limit);
         }
 
         // Always add the logged-in user because some tokenizers autosort them
         // first. They'll be filtered out on the client side if they don't
         // match the query.
         $ids[] = $request->getUser()->getID();
 
         if ($ids) {
           $users = id(new PhabricatorUser())->loadColumnsWhere(
             $columns,
             'id IN (%Ld)',
             $ids);
         } else {
           $users = array();
         }
       } else {
         $users = id(new PhabricatorUser())->loadColumns($columns);
       }
 
       if ($need_rich_data) {
         $phids = mpull($users, 'getPHID');
         $handles = $this->loadViewerHandles($phids);
       }
 
       foreach ($users as $user) {
         if (!$need_all_users) {
           if (!$need_agents && $user->getIsSystemAgent()) {
             continue;
           }
           if ($user->getIsDisabled()) {
             continue;
           }
         }
 
         $result = id(new PhabricatorTypeaheadResult())
           ->setName($user->getFullName())
           ->setURI('/p/'.$user->getUsername())
           ->setPHID($user->getPHID())
           ->setPriorityString($user->getUsername());
 
         if ($need_rich_data) {
           $display_type = 'User';
           if ($user->getIsAdmin()) {
             $display_type = 'Administrator';
           }
           $result->setDisplayType($display_type);
           $result->setImageURI($handles[$user->getPHID()]->getImageURI());
           $result->setPriorityType('user');
         }
         $results[] = $result;
       }
     }
 
     if ($need_lists) {
       $lists = id(new PhabricatorMailingListQuery())
         ->setViewer($viewer)
         ->execute();
       foreach ($lists as $list) {
         $results[] = id(new PhabricatorTypeaheadResult())
           ->setName($list->getName())
           ->setURI($list->getURI())
           ->setPHID($list->getPHID());
       }
     }
 
+    if ($need_build_plans) {
+      $plans = id(new HarbormasterBuildPlanQuery())
+        ->setViewer($viewer)
+        ->execute();
+      foreach ($plans as $plan) {
+        $results[] = id(new PhabricatorTypeaheadResult())
+          ->setName($plan->getName())
+          ->setPHID($plan->getPHID());
+      }
+    }
+
     if ($need_projs) {
       $projs = id(new PhabricatorProjectQuery())
         ->setViewer($viewer)
         ->withStatus(PhabricatorProjectQuery::STATUS_OPEN)
         ->needProfiles(true)
         ->execute();
       foreach ($projs as $proj) {
         $proj_result = id(new PhabricatorTypeaheadResult())
           ->setName($proj->getName())
           ->setDisplayType("Project")
           ->setURI('/project/view/'.$proj->getID().'/')
           ->setPHID($proj->getPHID());
 
         $prof = $proj->getProfile();
         $proj_result->setImageURI($prof->getProfileImageURI());
 
         $results[] = $proj_result;
       }
     }
 
     if ($need_repos) {
       $repos = id(new PhabricatorRepositoryQuery())
         ->setViewer($viewer)
         ->execute();
       foreach ($repos as $repo) {
         $results[] = id(new PhabricatorTypeaheadResult())
           ->setName('r'.$repo->getCallsign().' ('.$repo->getName().')')
           ->setURI('/diffusion/'.$repo->getCallsign().'/')
           ->setPHID($repo->getPHID())
           ->setPriorityString('r'.$repo->getCallsign());
       }
     }
 
     if ($need_packages) {
       $packages = id(new PhabricatorOwnersPackage())->loadAll();
       foreach ($packages as $package) {
         $results[] = id(new PhabricatorTypeaheadResult())
           ->setName($package->getName())
           ->setURI('/owners/package/'.$package->getID().'/')
           ->setPHID($package->getPHID());
       }
     }
 
     if ($need_arcanist_projects) {
       $arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll();
       foreach ($arcprojs as $proj) {
         $results[] = id(new PhabricatorTypeaheadResult())
           ->setName($proj->getName())
           ->setPHID($proj->getPHID());
       }
     }
 
     if ($need_applications) {
       $applications = PhabricatorApplication::getAllInstalledApplications();
       foreach ($applications as $application) {
         $uri = $application->getTypeaheadURI();
         if (!$uri) {
           continue;
         }
         $name = $application->getName().' '.$application->getShortDescription();
 
         $results[] = id(new PhabricatorTypeaheadResult())
           ->setName($name)
           ->setURI($uri)
           ->setPHID($application->getPHID())
           ->setPriorityString($application->getName())
           ->setDisplayName($application->getName())
           ->setDisplayType($application->getShortDescription())
           ->setImageuRI($application->getIconURI())
           ->setPriorityType('apps');
       }
     }
 
     if ($need_symbols) {
       $symbols = id(new DiffusionSymbolQuery())
         ->setNamePrefix($query)
         ->setLimit(15)
         ->needArcanistProjects(true)
         ->needRepositories(true)
         ->needPaths(true)
         ->execute();
       foreach ($symbols as $symbol) {
         $lang = $symbol->getSymbolLanguage();
         $name = $symbol->getSymbolName();
         $type = $symbol->getSymbolType();
         $proj = $symbol->getArcanistProject()->getName();
 
         $results[] = id(new PhabricatorTypeaheadResult())
           ->setName($name)
           ->setURI($symbol->getURI())
           ->setPHID(md5($symbol->getURI())) // Just needs to be unique.
           ->setDisplayName($name)
           ->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$proj.')')
           ->setPriorityType('symb');
       }
     }
 
     if ($need_jump_objects) {
       $objects = id(new PhabricatorObjectQuery())
         ->setViewer($viewer)
         ->withNames(array($raw_query))
         ->execute();
       if ($objects) {
         $handles = id(new PhabricatorHandleQuery())
           ->setViewer($viewer)
           ->withPHIDs(mpull($objects, 'getPHID'))
           ->execute();
         $handle = head($handles);
         if ($handle) {
           $results[] = id(new PhabricatorTypeaheadResult())
             ->setName($handle->getFullName())
             ->setDisplayType($handle->getTypeName())
             ->setURI($handle->getURI())
             ->setPHID($handle->getPHID())
             ->setPriorityType('jump');
         }
       }
     }
 
     $content = mpull($results, 'getWireFormat');
 
     if ($request->isAjax()) {
       return id(new AphrontAjaxResponse())->setContent($content);
     }
 
     // If there's a non-Ajax request to this endpoint, show results in a tabular
     // format to make it easier to debug typeahead output.
 
     $rows = array();
     foreach ($results as $result) {
       $wire = $result->getWireFormat();
       $rows[] = $wire;
     }
 
     $table = new AphrontTableView($rows);
     $table->setHeaders(
       array(
         'Name',
         'URI',
         'PHID',
         'Priority',
         'Display Name',
         'Display Type',
         'Image URI',
         'Priority Type',
       ));
 
     $panel = new AphrontPanelView();
     $panel->setHeader('Typeahead Results');
     $panel->appendChild($table);
 
     return $this->buildStandardPageResponse(
       $panel,
       array(
         'title' => 'Typeahead Results',
       ));
   }
 
 }
diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
index 35b7a01e9..7d8cde287 100644
--- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
+++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
@@ -1,378 +1,379 @@
 /**
  * @requires multirow-row-manager
  *           javelin-install
  *           javelin-typeahead
  *           javelin-util
  *           javelin-dom
  *           javelin-tokenizer
  *           javelin-typeahead-preloaded-source
  *           javelin-stratcom
  *           javelin-json
  *           phabricator-prefab
  * @provides herald-rule-editor
  * @javelin
  */
 
 JX.install('HeraldRuleEditor', {
   construct : function(config) {
     var root = JX.$(config.root);
     this._root = root;
 
     JX.DOM.listen(
       root,
       'click',
       'create-condition',
       JX.bind(this, this._onnewcondition));
 
     JX.DOM.listen(
       root,
       'click',
       'create-action',
       JX.bind(this, this._onnewaction));
 
     JX.DOM.listen(root, 'change', null, JX.bind(this, this._onchange));
     JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit));
 
     var conditionsTable = JX.DOM.find(root, 'table', 'rule-conditions');
     var actionsTable = JX.DOM.find(root, 'table', 'rule-actions');
 
     this._conditionsRowManager = new JX.MultirowRowManager(conditionsTable);
     this._conditionsRowManager.listen(
       'row-removed',
       JX.bind(this, function(row_id) {
           delete this._config.conditions[row_id];
         }));
 
     this._actionsRowManager = new JX.MultirowRowManager(actionsTable);
     this._actionsRowManager.listen(
       'row-removed',
       JX.bind(this, function(row_id) {
           delete this._config.actions[row_id];
         }));
 
     this._conditionGetters = {};
     this._conditionTypes = {};
     this._actionGetters = {};
     this._actionTypes = {};
 
     this._config = config;
 
     var conditions = this._config.conditions;
     this._config.conditions = [];
 
     var actions = this._config.actions;
     this._config.actions = [];
 
     this._renderConditions(conditions);
     this._renderActions(actions);
   },
 
   members : {
     _config : null,
     _root : null,
     _conditionGetters : null,
     _conditionTypes : null,
     _actionGetters : null,
     _actionTypes : null,
     _conditionsRowManager : null,
     _actionsRowManager : null,
 
     _onnewcondition : function(e) {
       this._newCondition();
       e.kill();
     },
     _onnewaction : function(e) {
       this._newAction();
       e.kill();
     },
     _onchange : function(e) {
       var target = e.getTarget();
 
       var row = e.getNode(JX.MultirowRowManager.getRowSigil());
       if (!row) {
         // Changing the "when all of / any of these..." dropdown.
         return;
       }
 
       if (JX.Stratcom.hasSigil(target, 'field-select')) {
         this._onfieldchange(row);
       } else if (JX.Stratcom.hasSigil(target, 'condition-select')) {
         this._onconditionchange(row);
       } else if (JX.Stratcom.hasSigil(target, 'action-select')) {
         this._onactionchange(row);
       }
     },
     _onsubmit : function(e) {
       var rule = JX.DOM.find(this._root, 'input', 'rule');
 
       var k;
 
       for (k in this._config.conditions) {
         this._config.conditions[k][2] = this._getConditionValue(k);
       }
 
       var acts = this._config.actions;
       for (k in this._config.actions) {
         this._config.actions[k][1] = this._getActionTarget(k);
       }
       rule.value = JX.JSON.stringify({
         conditions: this._config.conditions,
         actions: this._config.actions
       });
     },
 
     _getConditionValue : function(id) {
       if (this._conditionGetters[id]) {
         return this._conditionGetters[id]();
       }
       return this._config.conditions[id][2];
     },
 
     _getActionTarget : function(id) {
       if (this._actionGetters[id]) {
         return this._actionGetters[id]();
       }
       return this._config.actions[id][1];
     },
 
     _onactionchange : function(r) {
       var target = JX.DOM.find(r, 'select', 'action-select');
       var row_id = this._actionsRowManager.getRowID(r);
 
       this._config.actions[row_id][0] = target.value;
 
       var target_cell = JX.DOM.find(r, 'td', 'target-cell');
       var target_input = this._renderTargetInputForRow(row_id);
 
       JX.DOM.setContent(target_cell, target_input);
     },
     _onfieldchange : function(r) {
       var target = JX.DOM.find(r, 'select', 'field-select');
       var row_id = this._actionsRowManager.getRowID(r);
 
       this._config.conditions[row_id][0] = target.value;
 
       var condition_cell = JX.DOM.find(r, 'td', 'condition-cell');
       var condition_select = this._renderSelect(
         this._selectKeys(
           this._config.info.conditions,
           this._config.info.conditionMap[target.value]),
         this._config.conditions[row_id][1],
         'condition-select');
 
       JX.DOM.setContent(condition_cell, condition_select);
 
       this._onconditionchange(r);
 
       var condition_name = this._config.conditions[row_id][1];
       if (condition_name == 'unconditionally') {
         JX.DOM.hide(condition_select);
       }
     },
     _onconditionchange : function(r) {
       var target = JX.DOM.find(r, 'select', 'condition-select');
       var row_id = this._conditionsRowManager.getRowID(r);
 
       this._config.conditions[row_id][1] = target.value;
 
       var value_cell = JX.DOM.find(r, 'td', 'value-cell');
       var value_input = this._renderValueInputForRow(row_id);
       JX.DOM.setContent(value_cell, value_input);
     },
 
     _renderTargetInputForRow : function(row_id) {
       var action = this._config.actions[row_id];
       var type = this._config.info.targets[action[0]];
 
       var input = this._buildInput(type);
       var node = input[0];
       var get_fn = input[1];
       var set_fn = input[2];
 
       if (node) {
         JX.Stratcom.addSigil(node, 'action-target');
       }
 
       var old_type = this._actionTypes[row_id];
       if (old_type == type || !old_type) {
         set_fn(this._getActionTarget(row_id));
       }
 
       this._actionTypes[row_id] = type;
       this._actionGetters[row_id] = get_fn;
 
       return node;
     },
 
     _buildInput : function(type) {
       var input;
       var get_fn;
       var set_fn;
       switch (type) {
         case 'rule':
           input = this._renderSelect(this._config.template.rules);
           get_fn = function() { return input.value; };
           set_fn = function(v) { input.value = v; };
           break;
         case 'email':
         case 'user':
         case 'repository':
         case 'tag':
         case 'package':
         case 'project':
         case 'userorproject':
+        case 'buildplan':
           var tokenizer = this._newTokenizer(type);
           input = tokenizer[0];
           get_fn = tokenizer[1];
           set_fn = tokenizer[2];
           break;
         case 'none':
           input = '';
           get_fn = JX.bag;
           set_fn = JX.bag;
           break;
         case 'contentsource':
           input = this._renderSelect(this._config.template.contentSources);
           get_fn = function() { return input.value; };
           set_fn = function(v) { input.value = v; };
           set_fn(this._config.template.defaultSource);
           break;
         case 'flagcolor':
           input = this._renderSelect(this._config.template.colors);
           get_fn = function() { return input.value; };
           set_fn = function(v) { input.value = v; };
           set_fn(this._config.template.defaultColor);
           break;
         default:
           input = JX.$N('input', {type: 'text'});
           get_fn = function() { return input.value; };
           set_fn = function(v) { input.value = v; };
           break;
       }
 
       return [input, get_fn, set_fn];
     },
 
     _renderValueInputForRow : function(row_id) {
       var cond = this._config.conditions[row_id];
       var type = this._config.info.values[cond[0]][cond[1]];
 
       var input = this._buildInput(type);
       var node = input[0];
       var get_fn = input[1];
       var set_fn = input[2];
 
       if (node) {
         JX.Stratcom.addSigil(node, 'condition-value');
       }
 
       var old_type = this._conditionTypes[row_id];
       if (old_type == type || !old_type) {
         set_fn(this._getConditionValue(row_id));
       }
 
       this._conditionTypes[row_id] = type;
       this._conditionGetters[row_id] = get_fn;
 
       return node;
     },
 
     _newTokenizer : function(type, limit) {
       var template = JX.$N(
         'div',
         JX.$H(this._config.template.markup));
       template = template.firstChild;
       template.id = '';
 
       var datasource = new JX.TypeaheadPreloadedSource(
         this._config.template.source[type]);
 
       var typeahead = new JX.Typeahead(template);
       typeahead.setDatasource(datasource);
 
       var tokenizer = new JX.Tokenizer(template);
       tokenizer.setLimit(limit);
       tokenizer.setTypeahead(typeahead);
       tokenizer.start();
 
       return [
         template,
         function() {
           return tokenizer.getTokens();
         },
         function(map) {
           for (var k in map) {
             tokenizer.addToken(k, map[k]);
           }
         }];
     },
     _selectKeys : function(map, keys) {
       var r = {};
       for (var ii = 0; ii < keys.length; ii++) {
         r[keys[ii]] = map[keys[ii]];
       }
       return r;
     },
     _renderConditions : function(conditions) {
       for (var k in conditions) {
         this._newCondition(conditions[k]);
       }
     },
     _newCondition : function(data) {
       var row = this._conditionsRowManager.addRow([]);
       var row_id = this._conditionsRowManager.getRowID(row);
       this._config.conditions[row_id] = data || [null, null, ''];
       var r = this._conditionsRowManager.updateRow(
         row_id,
         this._renderCondition(row_id));
 
       this._onfieldchange(r);
     },
     _renderCondition : function(row_id) {
       var field_select = this._renderSelect(
         this._config.info.fields,
         this._config.conditions[row_id][0],
         'field-select');
       var field_cell = JX.$N('td', {sigil: 'field-cell'}, field_select);
 
       var condition_cell = JX.$N('td', {sigil: 'condition-cell'});
       var value_cell = JX.$N('td', {className : 'value', sigil: 'value-cell'});
 
       return [field_cell, condition_cell, value_cell];
     },
     _renderActions : function(actions) {
       for (var k in actions) {
         this._newAction(actions[k]);
         delete actions[k];
       }
     },
     _newAction : function(data) {
       data = data || [];
       var temprow = this._actionsRowManager.addRow([]);
       var row_id = this._actionsRowManager.getRowID(temprow);
       this._config.actions[row_id] = data;
       var r = this._actionsRowManager.updateRow(row_id,
                                                 this._renderAction(data));
       this._onactionchange(r);
     },
     _renderAction : function(action) {
       var action_select = this._renderSelect(
         this._config.info.actions,
         action[0],
         'action-select');
       var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select);
 
       var target_cell = JX.$N(
         'td',
         {className : 'target', sigil : 'target-cell'});
 
       return [action_cell, target_cell];
     },
     _renderSelect : function(map, selected, sigil) {
       var attrs = {
         sigil : sigil
       };
       return JX.Prefab.renderSelect(map, selected, attrs);
     }
   }
 });