diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php
index e81031e16..4ee8a6372 100644
--- a/src/applications/feed/story/PhabricatorFeedStory.php
+++ b/src/applications/feed/story/PhabricatorFeedStory.php
@@ -1,379 +1,428 @@
 <?php
 
 /**
  * Manages rendering and aggregation of a story. A story is an event (like a
  * user adding a comment) which may be represented in different forms on
  * different channels (like feed, notifications and realtime alerts).
  *
  * @task load     Loading Stories
  * @task policy   Policy Implementation
  */
 abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
 
   private $data;
   private $hasViewed;
   private $framed;
   private $hovercard = false;
   private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML;
 
-  private $handles  = array();
-  private $objects  = array();
+  private $handles = array();
+  private $objects = array();
+  private $projectPHIDs = array();
 
 /* -(  Loading Stories  )---------------------------------------------------- */
 
 
   /**
    * Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
    * construct appropriate @{class:PhabricatorFeedStory} wrappers for each
    * data row.
    *
    * @param list<dict>  List of @{class:PhabricatorFeedStoryData} rows from the
    *                    database.
    * @return list<PhabricatorFeedStory>   List of @{class:PhabricatorFeedStory}
    *                                      objects.
    * @task load
    */
   public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
     $stories = array();
 
     $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
     foreach ($data as $story_data) {
       $class = $story_data->getStoryType();
 
       try {
         $ok =
           class_exists($class) &&
           is_subclass_of($class, 'PhabricatorFeedStory');
       } catch (PhutilMissingSymbolException $ex) {
         $ok = false;
       }
 
       // If the story type isn't a valid class or isn't a subclass of
       // PhabricatorFeedStory, decline to load it.
       if (!$ok) {
         continue;
       }
 
       $key = $story_data->getChronologicalKey();
       $stories[$key] = newv($class, array($story_data));
     }
 
     $object_phids = array();
     $key_phids = array();
     foreach ($stories as $key => $story) {
       $phids = array();
       foreach ($story->getRequiredObjectPHIDs() as $phid) {
         $phids[$phid] = true;
       }
       if ($story->getPrimaryObjectPHID()) {
         $phids[$story->getPrimaryObjectPHID()] = true;
       }
       $key_phids[$key] = $phids;
       $object_phids += $phids;
     }
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($viewer)
       ->withPHIDs(array_keys($object_phids))
       ->execute();
 
     foreach ($key_phids as $key => $phids) {
       if (!$phids) {
         continue;
       }
       $story_objects = array_select_keys($objects, array_keys($phids));
       if (count($story_objects) != count($phids)) {
         // An object this story requires either does not exist or is not visible
         // to the user. Decline to render the story.
         unset($stories[$key]);
         unset($key_phids[$key]);
         continue;
       }
 
       $stories[$key]->setObjects($story_objects);
     }
 
+    // If stories are about PhabricatorProjectInterface objects, load the
+    // projects the objects are a part of so we can render project tags
+    // on the stories.
+
+    $project_phids = array();
+    foreach ($objects as $object) {
+      if ($object instanceof PhabricatorProjectInterface) {
+        $project_phids[$object->getPHID()] = array();
+      }
+    }
+
+    if ($project_phids) {
+      $edge_query = id(new PhabricatorEdgeQuery())
+        ->withSourcePHIDs(array_keys($project_phids))
+        ->withEdgeTypes(
+          array(
+            PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
+          ));
+      $edge_query->execute();
+      foreach ($project_phids as $phid => $ignored) {
+        $project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid));
+      }
+    }
+
     $handle_phids = array();
     foreach ($stories as $key => $story) {
       foreach ($story->getRequiredHandlePHIDs() as $phid) {
         $key_phids[$key][$phid] = true;
       }
       if ($story->getAuthorPHID()) {
         $key_phids[$key][$story->getAuthorPHID()] = true;
       }
+
+      $object_phid = $story->getPrimaryObjectPHID();
+      $object_project_phids = idx($project_phids, $object_phid, array());
+      $story->setProjectPHIDs($object_project_phids);
+      foreach ($object_project_phids as $dst) {
+        $key_phids[$key][$dst] = true;
+      }
+
       $handle_phids += $key_phids[$key];
     }
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($viewer)
       ->withPHIDs(array_keys($handle_phids))
       ->execute();
 
     foreach ($key_phids as $key => $phids) {
       if (!$phids) {
         continue;
       }
       $story_handles = array_select_keys($handles, array_keys($phids));
       $stories[$key]->setHandles($story_handles);
     }
 
     return $stories;
   }
 
   public function setHovercard($hover) {
     $this->hovercard = $hover;
     return $this;
   }
 
   public function setRenderingTarget($target) {
     $this->validateRenderingTarget($target);
     $this->renderingTarget = $target;
     return $this;
   }
 
   public function getRenderingTarget() {
     return $this->renderingTarget;
   }
 
   private function validateRenderingTarget($target) {
     switch ($target) {
       case PhabricatorApplicationTransaction::TARGET_HTML:
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         break;
       default:
         throw new Exception('Unknown rendering target: '.$target);
         break;
     }
   }
 
   public function setObjects(array $objects) {
     $this->objects = $objects;
     return $this;
   }
 
   public function getObject($phid) {
     $object = idx($this->objects, $phid);
     if (!$object) {
       throw new Exception(
         "Story is asking for an object it did not request ('{$phid}')!");
     }
     return $object;
   }
 
   public function getPrimaryObject() {
     $phid = $this->getPrimaryObjectPHID();
     if (!$phid) {
       throw new Exception('Story has no primary object!');
     }
     return $this->getObject($phid);
   }
 
   public function getPrimaryObjectPHID() {
     return null;
   }
 
   final public function __construct(PhabricatorFeedStoryData $data) {
     $this->data = $data;
   }
 
   abstract public function renderView();
 
   public function getRequiredHandlePHIDs() {
     return array();
   }
 
   public function getRequiredObjectPHIDs() {
     return array();
   }
 
   public function setHasViewed($has_viewed) {
     $this->hasViewed = $has_viewed;
     return $this;
   }
 
   public function getHasViewed() {
     return $this->hasViewed;
   }
 
   final public function setFramed($framed) {
     $this->framed = $framed;
     return $this;
   }
 
   final public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   final protected function getObjects() {
     return $this->objects;
   }
 
   final protected function getHandles() {
     return $this->handles;
   }
 
   final protected function getHandle($phid) {
     if (isset($this->handles[$phid])) {
       if ($this->handles[$phid] instanceof PhabricatorObjectHandle) {
         return $this->handles[$phid];
       }
     }
 
     $handle = new PhabricatorObjectHandle();
     $handle->setPHID($phid);
     $handle->setName("Unloaded Object '{$phid}'");
 
     return $handle;
   }
 
   final public function getStoryData() {
     return $this->data;
   }
 
   final public function getEpoch() {
     return $this->getStoryData()->getEpoch();
   }
 
   final public function getChronologicalKey() {
     return $this->getStoryData()->getChronologicalKey();
   }
 
   final public function getValue($key, $default = null) {
     return $this->getStoryData()->getValue($key, $default);
   }
 
   final public function getAuthorPHID() {
     return $this->getStoryData()->getAuthorPHID();
   }
 
   final protected function renderHandleList(array $phids) {
     $items = array();
     foreach ($phids as $phid) {
       $items[] = $this->linkTo($phid);
     }
     $list = null;
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         $list = implode(', ', $items);
         break;
       case PhabricatorApplicationTransaction::TARGET_HTML:
         $list = phutil_implode_html(', ', $items);
         break;
     }
     return $list;
   }
 
   final protected function linkTo($phid) {
     $handle = $this->getHandle($phid);
 
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         return $handle->getLinkName();
     }
 
     // NOTE: We render our own link here to customize the styling and add
     // the '_top' target for framed feeds.
 
     $class = null;
     if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) {
       $class = 'phui-link-person';
     }
 
     return javelin_tag(
       'a',
       array(
         'href'    => $handle->getURI(),
         'target'  => $this->framed ? '_top' : null,
         'sigil'   => $this->hovercard ? 'hovercard' : null,
         'meta'    => $this->hovercard ? array('hoverPHID' => $phid) : null,
         'class'   => $class,
       ),
       $handle->getLinkName());
   }
 
   final protected function renderString($str) {
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_TEXT:
         return $str;
       case PhabricatorApplicationTransaction::TARGET_HTML:
         return phutil_tag('strong', array(), $str);
     }
   }
 
   final protected function renderSummary($text, $len = 128) {
     if ($len) {
       $text = phutil_utf8_shorten($text, $len);
     }
     switch ($this->getRenderingTarget()) {
       case PhabricatorApplicationTransaction::TARGET_HTML:
         $text = phutil_escape_html_newlines($text);
         break;
     }
     return $text;
   }
 
   public function getNotificationAggregations() {
     return array();
   }
 
   protected function newStoryView() {
-    return id(new PHUIFeedStoryView())
+    $view = id(new PHUIFeedStoryView())
       ->setChronologicalKey($this->getChronologicalKey())
       ->setEpoch($this->getEpoch())
       ->setViewed($this->getHasViewed());
+
+    $project_phids = $this->getProjectPHIDs();
+    if ($project_phids) {
+      $view->setTags($this->renderHandleList($project_phids));
+    }
+
+    return $view;
+  }
+
+  public function setProjectPHIDs(array $phids) {
+    $this->projectPHIDs = $phids;
+    return $this;
+  }
+
+  public function getProjectPHIDs() {
+    return $this->projectPHIDs;
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
   public function getPHID() {
     return null;
   }
 
   /**
    * @task policy
    */
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
 
   /**
    * @task policy
    */
   public function getPolicy($capability) {
     // If this story's primary object is a policy-aware object, use its policy
     // to control story visiblity.
 
     $primary_phid = $this->getPrimaryObjectPHID();
     if (isset($this->objects[$primary_phid])) {
       $object = $this->objects[$primary_phid];
       if ($object instanceof PhabricatorPolicyInterface) {
         return $object->getPolicy($capability);
       }
     }
 
     // TODO: Remove this once all objects are policy-aware. For now, keep
     // respecting the `feed.public` setting.
     return PhabricatorEnv::getEnvConfig('feed.public')
       ? PhabricatorPolicies::POLICY_PUBLIC
       : PhabricatorPolicies::POLICY_USER;
   }
 
 
   /**
    * @task policy
    */
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 }
diff --git a/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php
index 60d71005a..de717e25d 100644
--- a/src/view/phui/PHUIFeedStoryView.php
+++ b/src/view/phui/PHUIFeedStoryView.php
@@ -1,275 +1,294 @@
 <?php
 
 final class PHUIFeedStoryView extends AphrontView {
 
   private $title;
   private $image;
   private $imageHref;
   private $appIcon;
   private $phid;
   private $epoch;
   private $viewed;
   private $href;
   private $pontification = null;
   private $tokenBar = array();
   private $projects = array();
   private $actions = array();
   private $chronologicalKey;
+  private $tags;
+
+  public function setTags($tags) {
+    $this->tags = $tags;
+    return $this;
+  }
+
+  public function getTags() {
+    return $this->tags;
+  }
 
   public function setChronologicalKey($chronological_key) {
     $this->chronologicalKey = $chronological_key;
     return $this;
   }
 
   public function getChronologicalKey() {
     return $this->chronologicalKey;
   }
 
   public function setTitle($title) {
     $this->title = $title;
     return $this;
   }
 
   public function getTitle() {
     return $this->title;
   }
 
   public function setEpoch($epoch) {
     $this->epoch = $epoch;
     return $this;
   }
 
   public function setImage($image) {
     $this->image = $image;
     return $this;
   }
 
   public function setImageHref($image_href) {
     $this->imageHref = $image_href;
     return $this;
   }
 
   public function setAppIcon($icon) {
     $this->appIcon = $icon;
     return $this;
   }
 
   public function setViewed($viewed) {
     $this->viewed = $viewed;
     return $this;
   }
 
   public function getViewed() {
     return $this->viewed;
   }
 
   public function setHref($href) {
     $this->href = $href;
     return $this;
   }
 
   public function setTokenBar(array $tokens) {
     $this->tokenBar = $tokens;
     return $this;
   }
 
   public function addProject($project) {
     $this->projects[] = $project;
     return $this;
   }
 
   public function addAction(PHUIIconView $action) {
     $this->actions[] = $action;
     return $this;
   }
 
   public function setPontification($text, $title = null) {
     if ($title) {
       $title = phutil_tag('h3', array(), $title);
     }
     $copy = phutil_tag(
       'div',
         array(
           'class' => 'phui-feed-story-bigtext-post',
         ),
         array(
           $title,
           $text));
     $this->appendChild($copy);
     return $this;
   }
 
   public function getHref() {
     return $this->href;
   }
 
   public function renderNotification($user) {
     $classes = array(
       'phabricator-notification',
     );
 
     if (!$this->viewed) {
       $classes[] = 'phabricator-notification-unread';
     }
     if ($this->epoch) {
       if ($user) {
         $foot = phabricator_datetime($this->epoch, $user);
         $foot = phutil_tag(
           'span',
           array(
             'class' => 'phabricator-notification-date'),
           $foot);
       } else {
         $foot = null;
       }
     } else {
       $foot = pht('No time specified.');
     }
 
     return javelin_tag(
       'div',
       array(
         'class' => implode(' ', $classes),
         'sigil' => 'notification',
         'meta' => array(
           'href' => $this->getHref(),
         ),
       ),
       array($this->title, $foot));
   }
 
   public function render() {
 
     require_celerity_resource('phui-feed-story-css');
     Javelin::initBehavior('phabricator-hovercards');
 
     $body = null;
     $foot = null;
     $image_style = null;
     $actor = '';
 
     if ($this->image) {
       $actor = new PHUIIconView();
       $actor->setImage($this->image);
       $actor->addClass('phui-feed-story-actor-image');
       if ($this->imageHref) {
         $actor->setHref($this->imageHref);
       }
     }
 
     if ($this->epoch) {
       // TODO: This is really bad; when rendering through Conduit and via
       // renderText() we don't have a user.
       if ($this->user) {
         $foot = phabricator_datetime($this->epoch, $this->user);
       } else {
         $foot = null;
       }
     } else {
       $foot = pht('No time specified.');
     }
 
     if ($this->chronologicalKey) {
       $foot = phutil_tag(
         'a',
         array(
           'href' => '/feed/'.$this->chronologicalKey.'/',
         ),
         $foot);
     }
 
     $icon = null;
     if ($this->appIcon) {
       $icon = new PHUIIconView();
       $icon->setSpriteIcon($this->appIcon);
       $icon->setSpriteSheet(PHUIIconView::SPRITE_APPS);
     }
 
     $action_list = array();
     $icons = null;
     foreach ($this->actions as $action) {
       $action_list[] = phutil_tag(
         'li',
           array(
           'class' => 'phui-feed-story-action-item'
         ),
         $action);
     }
     if (!empty($action_list)) {
       $icons = phutil_tag(
         'ul',
           array(
             'class' => 'phui-feed-story-action-list'
           ),
           $action_list);
     }
 
     $head = phutil_tag(
       'div',
       array(
         'class' => 'phui-feed-story-head',
       ),
       array(
         $actor,
         nonempty($this->title, pht('Untitled Story')),
         $icons,
       ));
 
     if (!empty($this->tokenBar)) {
       $tokenview = phutil_tag(
         'div',
           array(
             'class' => 'phui-feed-token-bar'
           ),
         $this->tokenBar);
       $this->appendChild($tokenview);
     }
 
     $body_content = $this->renderChildren();
     if ($body_content) {
       $body = phutil_tag(
         'div',
         array(
           'class' => 'phui-feed-story-body',
         ),
         $body_content);
     }
 
+    $tags = null;
+    if ($this->tags) {
+      $tags = array(
+        " \xC2\xB7 ",
+        $this->tags);
+    }
+
     $foot = phutil_tag(
       'div',
       array(
         'class' => 'phui-feed-story-foot',
       ),
       array(
         $icon,
-        $foot));
+        $foot,
+        $tags,
+      ));
 
     $classes = array('phui-feed-story');
 
     return id(new PHUIBoxView())
       ->addClass(implode(' ', $classes))
       ->setBorder(true)
       ->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM)
       ->appendChild(array($head, $body, $foot));
   }
 
   public function setAppIconFromPHID($phid) {
     switch (phid_get_type($phid)) {
       case PholioMockPHIDType::TYPECONST:
         $this->setAppIcon('pholio-dark');
         break;
       case PhabricatorMacroMacroPHIDType::TYPECONST:
         $this->setAppIcon('macro-dark');
         break;
       case ManiphestTaskPHIDType::TYPECONST:
         $this->setAppIcon('maniphest-dark');
         break;
       case DifferentialRevisionPHIDType::TYPECONST:
         $this->setAppIcon('differential-dark');
         break;
       case PhabricatorCalendarEventPHIDType::TYPECONST:
         $this->setAppIcon('calendar-dark');
         break;
     }
   }
 }